From f925e43d1feb8da9ea118dde2ea4a63d5f760dbd Mon Sep 17 00:00:00 2001 From: Matthias Janetschek Date: Fri, 5 Jun 2015 15:58:53 +0200 Subject: [PATCH] . --- .../webresource/DeviceRepoHandler.cpp | 260 +----- .../webresource/DeviceRepoHandler.h | 141 +-- src/hos_cosgi_ruleengine/CMakeLists.txt | 1 + .../service/ruleengine/RuleEngineService.h | 31 +- src/hos_cosgi_ruleengine/package.xml | 1 + .../ruleengine/RuleEngineServiceImpl.h | 23 +- .../webresource/RuleEngineHandler.cpp | 10 + .../webresource/RuleEngineHandler.h | 106 +++ .../webroot/libs/dstore/.gitignore | 1 + .../webroot/libs/dstore/.jshintrc | 72 ++ .../webroot/libs/dstore/Cache.js | 196 +++++ .../webroot/libs/dstore/Csv.js | 172 ++++ .../webroot/libs/dstore/Filter.js | 91 ++ .../webroot/libs/dstore/LICENSE | 190 ++++ .../webroot/libs/dstore/LocalDB.js | 8 + .../webroot/libs/dstore/Memory.js | 238 +++++ .../webroot/libs/dstore/Promised.js | 36 + .../webroot/libs/dstore/QueryMethod.js | 68 ++ .../webroot/libs/dstore/QueryResults.js | 38 + .../webroot/libs/dstore/README.md | 67 ++ .../webroot/libs/dstore/Request.js | 296 +++++++ .../webroot/libs/dstore/RequestMemory.js | 13 + .../webroot/libs/dstore/Rest.js | 148 ++++ .../webroot/libs/dstore/SimpleQuery.js | 204 +++++ .../webroot/libs/dstore/Store.js | 497 +++++++++++ .../webroot/libs/dstore/Trackable.js | 426 +++++++++ .../webroot/libs/dstore/Tree.js | 42 + .../webroot/libs/dstore/bower.json | 21 + .../libs/dstore/charting/StoreSeries.js | 93 ++ .../webroot/libs/dstore/db/IndexedDB.js | 814 ++++++++++++++++++ .../webroot/libs/dstore/db/LocalStorage.js | 37 + .../webroot/libs/dstore/db/SQL.js | 439 ++++++++++ .../webroot/libs/dstore/db/has.js | 7 + .../webroot/libs/dstore/docs/Adapters.md | 60 ++ .../webroot/libs/dstore/docs/CONTRIBUTING.md | 240 ++++++ .../webroot/libs/dstore/docs/Collection.md | 183 ++++ .../webroot/libs/dstore/docs/Design.md | 25 + .../webroot/libs/dstore/docs/Plans.md | 5 + .../webroot/libs/dstore/docs/Store.md | 31 + .../webroot/libs/dstore/docs/Stores.md | 190 ++++ .../webroot/libs/dstore/docs/Testing.md | 45 + .../libs/dstore/extensions/RqlQuery.js | 13 + .../libs/dstore/legacy/DstoreAdapter.js | 178 ++++ .../libs/dstore/legacy/StoreAdapter.js | 128 +++ .../webroot/libs/dstore/package.js | 35 + .../webroot/libs/dstore/package.json | 32 + .../webroot/libs/dstore/tests/Cache.js | 232 +++++ .../webroot/libs/dstore/tests/Csv.js | 121 +++ .../webroot/libs/dstore/tests/LocalDB.js | 307 +++++++ .../webroot/libs/dstore/tests/LocalStorage.js | 189 ++++ .../webroot/libs/dstore/tests/Memory.js | 507 +++++++++++ .../webroot/libs/dstore/tests/Request.js | 369 ++++++++ .../libs/dstore/tests/RequestMemory.js | 157 ++++ .../webroot/libs/dstore/tests/Rest.js | 181 ++++ .../webroot/libs/dstore/tests/SimpleQuery.js | 89 ++ .../webroot/libs/dstore/tests/Store.js | 233 +++++ .../webroot/libs/dstore/tests/Trackable.js | 638 ++++++++++++++ .../webroot/libs/dstore/tests/Tree.js | 67 ++ .../webroot/libs/dstore/tests/all.js | 23 + .../libs/dstore/tests/charting/StoreSeries.js | 187 ++++ .../libs/dstore/tests/charting/stock.json | 8 + .../tests/charting/test_StoreSeries-amd.html | 184 ++++ .../libs/dstore/tests/data/contributors.csv | 620 +++++++++++++ .../webroot/libs/dstore/tests/data/items.json | 6 + .../webroot/libs/dstore/tests/data/node1.1 | 4 + .../webroot/libs/dstore/tests/data/node1.2 | 1 + .../libs/dstore/tests/data/noquote.csv | 4 + .../libs/dstore/tests/data/queryread.php | 47 + .../webroot/libs/dstore/tests/data/quote.csv | 12 + .../libs/dstore/tests/data/testData.js | 10 + .../libs/dstore/tests/data/treeTestRoot | 10 + .../libs/dstore/tests/extensions/RqlQuery.js | 50 ++ .../webroot/libs/dstore/tests/intern.js | 80 ++ .../tests/legacy/DstoreAdapter-Memory.js | 224 +++++ .../dstore/tests/legacy/DstoreAdapter-Rest.js | 126 +++ .../tests/legacy/StoreAdapter-AsyncMemory.js | 148 ++++ .../tests/legacy/StoreAdapter-DojoData.js | 148 ++++ .../tests/legacy/StoreAdapter-JsonRest.js | 111 +++ .../tests/legacy/StoreAdapter-Memory.js | 198 +++++ .../webroot/libs/dstore/tests/mockRequest.js | 79 ++ .../webroot/libs/dstore/tests/runTests.html | 9 + .../webroot/libs/dstore/tests/sorting.js | 98 +++ .../webroot/libs/dstore/typings/dstore.d.ts | 351 ++++++++ .../webresource/WebResourceHandlerImpl.h | 225 ++++- .../webserver/WebServerServiceImpl.cpp | 2 + 85 files changed, 11613 insertions(+), 394 deletions(-) create mode 100644 src/hos_cosgi_ruleengine/src/hos/cosgi/service/ruleengine/webresource/RuleEngineHandler.cpp create mode 100644 src/hos_cosgi_ruleengine/src/hos/cosgi/service/ruleengine/webresource/RuleEngineHandler.h create mode 100644 src/hos_cosgi_testwebapp/webroot/libs/dstore/.gitignore create mode 100644 src/hos_cosgi_testwebapp/webroot/libs/dstore/.jshintrc create mode 100644 src/hos_cosgi_testwebapp/webroot/libs/dstore/Cache.js create mode 100644 src/hos_cosgi_testwebapp/webroot/libs/dstore/Csv.js create mode 100644 src/hos_cosgi_testwebapp/webroot/libs/dstore/Filter.js create mode 100644 src/hos_cosgi_testwebapp/webroot/libs/dstore/LICENSE create mode 100644 src/hos_cosgi_testwebapp/webroot/libs/dstore/LocalDB.js create mode 100644 src/hos_cosgi_testwebapp/webroot/libs/dstore/Memory.js create mode 100644 src/hos_cosgi_testwebapp/webroot/libs/dstore/Promised.js create mode 100644 src/hos_cosgi_testwebapp/webroot/libs/dstore/QueryMethod.js create mode 100644 src/hos_cosgi_testwebapp/webroot/libs/dstore/QueryResults.js create mode 100644 src/hos_cosgi_testwebapp/webroot/libs/dstore/README.md create mode 100644 src/hos_cosgi_testwebapp/webroot/libs/dstore/Request.js create mode 100644 src/hos_cosgi_testwebapp/webroot/libs/dstore/RequestMemory.js create mode 100644 src/hos_cosgi_testwebapp/webroot/libs/dstore/Rest.js create mode 100644 src/hos_cosgi_testwebapp/webroot/libs/dstore/SimpleQuery.js create mode 100644 src/hos_cosgi_testwebapp/webroot/libs/dstore/Store.js create mode 100644 src/hos_cosgi_testwebapp/webroot/libs/dstore/Trackable.js create mode 100644 src/hos_cosgi_testwebapp/webroot/libs/dstore/Tree.js create mode 100644 src/hos_cosgi_testwebapp/webroot/libs/dstore/bower.json create mode 100644 src/hos_cosgi_testwebapp/webroot/libs/dstore/charting/StoreSeries.js create mode 100644 src/hos_cosgi_testwebapp/webroot/libs/dstore/db/IndexedDB.js create mode 100644 src/hos_cosgi_testwebapp/webroot/libs/dstore/db/LocalStorage.js create mode 100644 src/hos_cosgi_testwebapp/webroot/libs/dstore/db/SQL.js create mode 100644 src/hos_cosgi_testwebapp/webroot/libs/dstore/db/has.js create mode 100644 src/hos_cosgi_testwebapp/webroot/libs/dstore/docs/Adapters.md create mode 100644 src/hos_cosgi_testwebapp/webroot/libs/dstore/docs/CONTRIBUTING.md create mode 100644 src/hos_cosgi_testwebapp/webroot/libs/dstore/docs/Collection.md create mode 100644 src/hos_cosgi_testwebapp/webroot/libs/dstore/docs/Design.md create mode 100644 src/hos_cosgi_testwebapp/webroot/libs/dstore/docs/Plans.md create mode 100644 src/hos_cosgi_testwebapp/webroot/libs/dstore/docs/Store.md create mode 100644 src/hos_cosgi_testwebapp/webroot/libs/dstore/docs/Stores.md create mode 100644 src/hos_cosgi_testwebapp/webroot/libs/dstore/docs/Testing.md create mode 100644 src/hos_cosgi_testwebapp/webroot/libs/dstore/extensions/RqlQuery.js create mode 100644 src/hos_cosgi_testwebapp/webroot/libs/dstore/legacy/DstoreAdapter.js create mode 100644 src/hos_cosgi_testwebapp/webroot/libs/dstore/legacy/StoreAdapter.js create mode 100644 src/hos_cosgi_testwebapp/webroot/libs/dstore/package.js create mode 100644 src/hos_cosgi_testwebapp/webroot/libs/dstore/package.json create mode 100644 src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/Cache.js create mode 100644 src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/Csv.js create mode 100644 src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/LocalDB.js create mode 100644 src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/LocalStorage.js create mode 100644 src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/Memory.js create mode 100644 src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/Request.js create mode 100644 src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/RequestMemory.js create mode 100644 src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/Rest.js create mode 100644 src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/SimpleQuery.js create mode 100644 src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/Store.js create mode 100644 src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/Trackable.js create mode 100644 src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/Tree.js create mode 100644 src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/all.js create mode 100644 src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/charting/StoreSeries.js create mode 100644 src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/charting/stock.json create mode 100644 src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/charting/test_StoreSeries-amd.html create mode 100644 src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/data/contributors.csv create mode 100644 src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/data/items.json create mode 100644 src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/data/node1.1 create mode 100644 src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/data/node1.2 create mode 100644 src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/data/noquote.csv create mode 100644 src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/data/queryread.php create mode 100644 src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/data/quote.csv create mode 100644 src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/data/testData.js create mode 100644 src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/data/treeTestRoot create mode 100644 src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/extensions/RqlQuery.js create mode 100644 src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/intern.js create mode 100644 src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/legacy/DstoreAdapter-Memory.js create mode 100644 src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/legacy/DstoreAdapter-Rest.js create mode 100644 src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/legacy/StoreAdapter-AsyncMemory.js create mode 100644 src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/legacy/StoreAdapter-DojoData.js create mode 100644 src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/legacy/StoreAdapter-JsonRest.js create mode 100644 src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/legacy/StoreAdapter-Memory.js create mode 100644 src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/mockRequest.js create mode 100644 src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/runTests.html create mode 100644 src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/sorting.js create mode 100644 src/hos_cosgi_testwebapp/webroot/libs/dstore/typings/dstore.d.ts diff --git a/src/hos_cosgi_devicerepository/src/hos/cosgi/service/devicerepository/webresource/DeviceRepoHandler.cpp b/src/hos_cosgi_devicerepository/src/hos/cosgi/service/devicerepository/webresource/DeviceRepoHandler.cpp index 5b256558..29252cf6 100644 --- a/src/hos_cosgi_devicerepository/src/hos/cosgi/service/devicerepository/webresource/DeviceRepoHandler.cpp +++ b/src/hos_cosgi_devicerepository/src/hos/cosgi/service/devicerepository/webresource/DeviceRepoHandler.cpp @@ -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 DeviceRepoHandlerFactory::getDeviceEventQueue( const std::string& clientID ) { - if (clientID.empty()) { - return std::make_shared(clientID, repoService, this); - } else { - auto i = deviceEventQueues.find(clientID); - if (i == deviceEventQueues.end()) { - auto n = std::make_shared(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 _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 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 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 lock(_mutex); - _eventQueue.push(event); - } - _cv.notify_one(); -} - -libvariant::Variant DeviceEventQueue::popEvent() { - std::unique_lock lock(_mutex); - libvariant::Variant retval; - breakPopLoop = false; - Net::HTTPServerRequestImpl* reqImpl = static_cast(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::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::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 diff --git a/src/hos_cosgi_devicerepository/src/hos/cosgi/service/devicerepository/webresource/DeviceRepoHandler.h b/src/hos_cosgi_devicerepository/src/hos/cosgi/service/devicerepository/webresource/DeviceRepoHandler.h index ab518c10..8fd6127a 100644 --- a/src/hos_cosgi_devicerepository/src/hos/cosgi/service/devicerepository/webresource/DeviceRepoHandler.h +++ b/src/hos_cosgi_devicerepository/src/hos/cosgi/service/devicerepository/webresource/DeviceRepoHandler.h @@ -9,14 +9,6 @@ #include #include #include - - -#include -#include -#include -#include -#include -#include #include #include #include @@ -50,55 +42,31 @@ enum class DeviceAPIMethod { }; +typedef webserver::webresource::WebResourceEventQueue< + interface::Observer, + DeviceRepoHandlerFactory, + DeviceRepoHandler +> DeviceEventQueueBase; class DeviceEventQueue - : public virtual interface::Observer { + : 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 _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::observable_type* o, const devicerepository::DeviceEventType& key, @@ -107,42 +75,19 @@ public: - class DeviceRepoHandlerFactory - : public virtual webserver::WebResourceHandlerFactory, - public implementation::Service { + : public webserver::webresource::WebResourceHandlerFactoryImpl { public: typedef std::function infoCategoryFunc_t; private: - webserver::WebServerService* service = nullptr; DeviceRepositoryService* repoService; std::map infoCategoryFuncs; - std::map> deviceEventQueues; - boost::asio::io_service io_service; - std::unique_ptr 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 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& infoCategoryFuncs; -public: - DeviceRepoInfoHandler( Poco::URI matchUri, - DeviceAPIMethod method, - devicerepository::DeviceRepositoryService* repoService, - const std::map& 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& path = std::vector() ); -}; - - - -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& path = std::vector() ); -}; - - - -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 diff --git a/src/hos_cosgi_ruleengine/CMakeLists.txt b/src/hos_cosgi_ruleengine/CMakeLists.txt index 848e3f29..84a865fe 100644 --- a/src/hos_cosgi_ruleengine/CMakeLists.txt +++ b/src/hos_cosgi_ruleengine/CMakeLists.txt @@ -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 diff --git a/src/hos_cosgi_ruleengine/include/hos_cosgi_ruleengine/hos/cosgi/service/ruleengine/RuleEngineService.h b/src/hos_cosgi_ruleengine/include/hos_cosgi_ruleengine/hos/cosgi/service/ruleengine/RuleEngineService.h index a9e5fc5b..e325138d 100644 --- a/src/hos_cosgi_ruleengine/include/hos_cosgi_ruleengine/hos/cosgi/service/ruleengine/RuleEngineService.h +++ b/src/hos_cosgi_ruleengine/include/hos_cosgi_ruleengine/hos/cosgi/service/ruleengine/RuleEngineService.h @@ -6,6 +6,7 @@ #include #include #include +#include #include @@ -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 { public: diff --git a/src/hos_cosgi_ruleengine/package.xml b/src/hos_cosgi_ruleengine/package.xml index 33853b8c..af5b35ef 100644 --- a/src/hos_cosgi_ruleengine/package.xml +++ b/src/hos_cosgi_ruleengine/package.xml @@ -48,6 +48,7 @@ hos_deps roscpp hos_cpp_cron + hos_cosgi_webserver hos_celix hos_cosgi_devicerepository hos_cosgi_core diff --git a/src/hos_cosgi_ruleengine/src/hos/cosgi/service/ruleengine/RuleEngineServiceImpl.h b/src/hos_cosgi_ruleengine/src/hos/cosgi/service/ruleengine/RuleEngineServiceImpl.h index 11c5299d..a7358386 100644 --- a/src/hos_cosgi_ruleengine/src/hos/cosgi/service/ruleengine/RuleEngineServiceImpl.h +++ b/src/hos_cosgi_ruleengine/src/hos/cosgi/service/ruleengine/RuleEngineServiceImpl.h @@ -22,6 +22,21 @@ #include +namespace std { +/** + * Specialization of std::hash for RuleEngineEventType. + * + * Required by std::map. + */ +template <> +class hash { +public : + size_t operator()(const hos::cosgi::service::ruleengine::RuleEngineEventType& x) const { + return static_cast(x); + } +}; +} // namespace std + // Service Namespace namespace hos { @@ -40,9 +55,11 @@ class RulePackageImpl; /** * Implementation of RuleEngineService. **/ -class RuleEngineServiceImpl : - public virtual RuleEngineService, - public implementation::Service { +class RuleEngineServiceImpl + : public virtual RuleEngineService, + public implementation::Service, + public implementation::Observable { + //public implementation::ObservableService { /* Why does this one not work? */ private: typedef std::map> rulePkgMap_t; diff --git a/src/hos_cosgi_ruleengine/src/hos/cosgi/service/ruleengine/webresource/RuleEngineHandler.cpp b/src/hos_cosgi_ruleengine/src/hos/cosgi/service/ruleengine/webresource/RuleEngineHandler.cpp new file mode 100644 index 00000000..411d3e12 --- /dev/null +++ b/src/hos_cosgi_ruleengine/src/hos/cosgi/service/ruleengine/webresource/RuleEngineHandler.cpp @@ -0,0 +1,10 @@ +/* + * RuleEngineHandler.cpp + * + * Created on: 05.06.2015 + * Author: matthias + */ + + + + diff --git a/src/hos_cosgi_ruleengine/src/hos/cosgi/service/ruleengine/webresource/RuleEngineHandler.h b/src/hos_cosgi_ruleengine/src/hos/cosgi/service/ruleengine/webresource/RuleEngineHandler.h new file mode 100644 index 00000000..64f8eb59 --- /dev/null +++ b/src/hos_cosgi_ruleengine/src/hos/cosgi/service/ruleengine/webresource/RuleEngineHandler.h @@ -0,0 +1,106 @@ + +#pragma once + + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + + +// 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 { +private: + webserver::WebServerService* service = nullptr; + RuleEngineService* ruleService; + boost::asio::io_service io_service; + std::unique_ptr 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 + + diff --git a/src/hos_cosgi_testwebapp/webroot/libs/dstore/.gitignore b/src/hos_cosgi_testwebapp/webroot/libs/dstore/.gitignore new file mode 100644 index 00000000..40b878db --- /dev/null +++ b/src/hos_cosgi_testwebapp/webroot/libs/dstore/.gitignore @@ -0,0 +1 @@ +node_modules/ \ No newline at end of file diff --git a/src/hos_cosgi_testwebapp/webroot/libs/dstore/.jshintrc b/src/hos_cosgi_testwebapp/webroot/libs/dstore/.jshintrc new file mode 100644 index 00000000..f921bbf6 --- /dev/null +++ b/src/hos_cosgi_testwebapp/webroot/libs/dstore/.jshintrc @@ -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 +} \ No newline at end of file diff --git a/src/hos_cosgi_testwebapp/webroot/libs/dstore/Cache.js b/src/hos_cosgi_testwebapp/webroot/libs/dstore/Cache.js new file mode 100644 index 00000000..c970df4d --- /dev/null +++ b/src/hos_cosgi_testwebapp/webroot/libs/dstore/Cache.js @@ -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; +}); diff --git a/src/hos_cosgi_testwebapp/webroot/libs/dstore/Csv.js b/src/hos_cosgi_testwebapp/webroot/libs/dstore/Csv.js new file mode 100644 index 00000000..9f84c7fa --- /dev/null +++ b/src/hos_cosgi_testwebapp/webroot/libs/dstore/Csv.js @@ -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; + } + }); +}); \ No newline at end of file diff --git a/src/hos_cosgi_testwebapp/webroot/libs/dstore/Filter.js b/src/hos_cosgi_testwebapp/webroot/libs/dstore/Filter.js new file mode 100644 index 00000000..3e281163 --- /dev/null +++ b/src/hos_cosgi_testwebapp/webroot/libs/dstore/Filter.js @@ -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; +}); \ No newline at end of file diff --git a/src/hos_cosgi_testwebapp/webroot/libs/dstore/LICENSE b/src/hos_cosgi_testwebapp/webroot/libs/dstore/LICENSE new file mode 100644 index 00000000..b940f6a3 --- /dev/null +++ b/src/hos_cosgi_testwebapp/webroot/libs/dstore/LICENSE @@ -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. diff --git a/src/hos_cosgi_testwebapp/webroot/libs/dstore/LocalDB.js b/src/hos_cosgi_testwebapp/webroot/libs/dstore/LocalDB.js new file mode 100644 index 00000000..2fe73dc1 --- /dev/null +++ b/src/hos_cosgi_testwebapp/webroot/libs/dstore/LocalDB.js @@ -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; +}); diff --git a/src/hos_cosgi_testwebapp/webroot/libs/dstore/Memory.js b/src/hos_cosgi_testwebapp/webroot/libs/dstore/Memory.js new file mode 100644 index 00000000..7c310053 --- /dev/null +++ b/src/hos_cosgi_testwebapp/webroot/libs/dstore/Memory.js @@ -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); + } + }); +}); diff --git a/src/hos_cosgi_testwebapp/webroot/libs/dstore/Promised.js b/src/hos_cosgi_testwebapp/webroot/libs/dstore/Promised.js new file mode 100644 index 00000000..939ee441 --- /dev/null +++ b/src/hos_cosgi_testwebapp/webroot/libs/dstore/Promised.js @@ -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) + }); +}); diff --git a/src/hos_cosgi_testwebapp/webroot/libs/dstore/QueryMethod.js b/src/hos_cosgi_testwebapp/webroot/libs/dstore/QueryMethod.js new file mode 100644 index 00000000..4ffc2a19 --- /dev/null +++ b/src/hos_cosgi_testwebapp/webroot/libs/dstore/QueryMethod.js @@ -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; + }; + }; +}); diff --git a/src/hos_cosgi_testwebapp/webroot/libs/dstore/QueryResults.js b/src/hos_cosgi_testwebapp/webroot/libs/dstore/QueryResults.js new file mode 100644 index 00000000..7744e1bd --- /dev/null +++ b/src/hos_cosgi_testwebapp/webroot/libs/dstore/QueryResults.js @@ -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; + }; +}); diff --git a/src/hos_cosgi_testwebapp/webroot/libs/dstore/README.md b/src/hos_cosgi_testwebapp/webroot/libs/dstore/README.md new file mode 100644 index 00000000..3253e6de --- /dev/null +++ b/src/hos_cosgi_testwebapp/webroot/libs/dstore/README.md @@ -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. \ No newline at end of file diff --git a/src/hos_cosgi_testwebapp/webroot/libs/dstore/Request.js b/src/hos_cosgi_testwebapp/webroot/libs/dstore/Request.js new file mode 100644 index 00000000..92ca360d --- /dev/null +++ b/src/hos_cosgi_testwebapp/webroot/libs/dstore/Request.js @@ -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 + }; + } + }); + +}); \ No newline at end of file diff --git a/src/hos_cosgi_testwebapp/webroot/libs/dstore/RequestMemory.js b/src/hos_cosgi_testwebapp/webroot/libs/dstore/RequestMemory.js new file mode 100644 index 00000000..7a832a81 --- /dev/null +++ b/src/hos_cosgi_testwebapp/webroot/libs/dstore/RequestMemory.js @@ -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 + }); +}); diff --git a/src/hos_cosgi_testwebapp/webroot/libs/dstore/Rest.js b/src/hos_cosgi_testwebapp/webroot/libs/dstore/Rest.js new file mode 100644 index 00000000..26b884be --- /dev/null +++ b/src/hos_cosgi_testwebapp/webroot/libs/dstore/Rest.js @@ -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; + }); + } + }); + +}); diff --git a/src/hos_cosgi_testwebapp/webroot/libs/dstore/SimpleQuery.js b/src/hos_cosgi_testwebapp/webroot/libs/dstore/SimpleQuery.js new file mode 100644 index 00000000..93ce2b8b --- /dev/null +++ b/src/hos_cosgi_testwebapp/webroot/libs/dstore/SimpleQuery.js @@ -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 */ + }); +}); diff --git a/src/hos_cosgi_testwebapp/webroot/libs/dstore/Store.js b/src/hos_cosgi_testwebapp/webroot/libs/dstore/Store.js new file mode 100644 index 00000000..494f3dfc --- /dev/null +++ b/src/hos_cosgi_testwebapp/webroot/libs/dstore/Store.js @@ -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 + }; +====*/ diff --git a/src/hos_cosgi_testwebapp/webroot/libs/dstore/Trackable.js b/src/hos_cosgi_testwebapp/webroot/libs/dstore/Trackable.js new file mode 100644 index 00000000..c1c2d53f --- /dev/null +++ b/src/hos_cosgi_testwebapp/webroot/libs/dstore/Trackable.js @@ -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; +}); diff --git a/src/hos_cosgi_testwebapp/webroot/libs/dstore/Tree.js b/src/hos_cosgi_testwebapp/webroot/libs/dstore/Tree.js new file mode 100644 index 00000000..f50bd763 --- /dev/null +++ b/src/hos_cosgi_testwebapp/webroot/libs/dstore/Tree.js @@ -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) }); + } + }); +}); diff --git a/src/hos_cosgi_testwebapp/webroot/libs/dstore/bower.json b/src/hos_cosgi_testwebapp/webroot/libs/dstore/bower.json new file mode 100644 index 00000000..3b384d39 --- /dev/null +++ b/src/hos_cosgi_testwebapp/webroot/libs/dstore/bower.json @@ -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" ] +} \ No newline at end of file diff --git a/src/hos_cosgi_testwebapp/webroot/libs/dstore/charting/StoreSeries.js b/src/hos_cosgi_testwebapp/webroot/libs/dstore/charting/StoreSeries.js new file mode 100644 index 00000000..1dbdfe94 --- /dev/null +++ b/src/hos_cosgi_testwebapp/webroot/libs/dstore/charting/StoreSeries.js @@ -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(); + } + } + }); +}); diff --git a/src/hos_cosgi_testwebapp/webroot/libs/dstore/db/IndexedDB.js b/src/hos_cosgi_testwebapp/webroot/libs/dstore/db/IndexedDB.js new file mode 100644 index 00000000..a6f03904 --- /dev/null +++ b/src/hos_cosgi_testwebapp/webroot/libs/dstore/db/IndexedDB.js @@ -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}); + } + }); + +}); diff --git a/src/hos_cosgi_testwebapp/webroot/libs/dstore/db/LocalStorage.js b/src/hos_cosgi_testwebapp/webroot/libs/dstore/db/LocalStorage.js new file mode 100644 index 00000000..b9442bc8 --- /dev/null +++ b/src/hos_cosgi_testwebapp/webroot/libs/dstore/db/LocalStorage.js @@ -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); + } + }); +}); \ No newline at end of file diff --git a/src/hos_cosgi_testwebapp/webroot/libs/dstore/db/SQL.js b/src/hos_cosgi_testwebapp/webroot/libs/dstore/db/SQL.js new file mode 100644 index 00000000..94e9819d --- /dev/null +++ b/src/hos_cosgi_testwebapp/webroot/libs/dstore/db/SQL.js @@ -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; + } + + }); +}); diff --git a/src/hos_cosgi_testwebapp/webroot/libs/dstore/db/has.js b/src/hos_cosgi_testwebapp/webroot/libs/dstore/db/has.js new file mode 100644 index 00000000..03852749 --- /dev/null +++ b/src/hos_cosgi_testwebapp/webroot/libs/dstore/db/has.js @@ -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; +}); \ No newline at end of file diff --git a/src/hos_cosgi_testwebapp/webroot/libs/dstore/docs/Adapters.md b/src/hos_cosgi_testwebapp/webroot/libs/dstore/docs/Adapters.md new file mode 100644 index 00000000..04556c2b --- /dev/null +++ b/src/hos_cosgi_testwebapp/webroot/libs/dstore/docs/Adapters.md @@ -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. diff --git a/src/hos_cosgi_testwebapp/webroot/libs/dstore/docs/CONTRIBUTING.md b/src/hos_cosgi_testwebapp/webroot/libs/dstore/docs/CONTRIBUTING.md new file mode 100644 index 00000000..a9839f5e --- /dev/null +++ b/src/hos_cosgi_testwebapp/webroot/libs/dstore/docs/CONTRIBUTING.md @@ -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 ..." 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 \ No newline at end of file diff --git a/src/hos_cosgi_testwebapp/webroot/libs/dstore/docs/Collection.md b/src/hos_cosgi_testwebapp/webroot/libs/dstore/docs/Collection.md new file mode 100644 index 00000000..62a98887 --- /dev/null +++ b/src/hos_cosgi_testwebapp/webroot/libs/dstore/docs/Collection.md @@ -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 : []; + }; + } + }) diff --git a/src/hos_cosgi_testwebapp/webroot/libs/dstore/docs/Design.md b/src/hos_cosgi_testwebapp/webroot/libs/dstore/docs/Design.md new file mode 100644 index 00000000..4f5fb042 --- /dev/null +++ b/src/hos_cosgi_testwebapp/webroot/libs/dstore/docs/Design.md @@ -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 `'_createQuerier'`, 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. \ No newline at end of file diff --git a/src/hos_cosgi_testwebapp/webroot/libs/dstore/docs/Plans.md b/src/hos_cosgi_testwebapp/webroot/libs/dstore/docs/Plans.md new file mode 100644 index 00000000..c352be6b --- /dev/null +++ b/src/hos_cosgi_testwebapp/webroot/libs/dstore/docs/Plans.md @@ -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. \ No newline at end of file diff --git a/src/hos_cosgi_testwebapp/webroot/libs/dstore/docs/Store.md b/src/hos_cosgi_testwebapp/webroot/libs/dstore/docs/Store.md new file mode 100644 index 00000000..e3a69456 --- /dev/null +++ b/src/hos_cosgi_testwebapp/webroot/libs/dstore/docs/Store.md @@ -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. \ No newline at end of file diff --git a/src/hos_cosgi_testwebapp/webroot/libs/dstore/docs/Stores.md b/src/hos_cosgi_testwebapp/webroot/libs/dstore/docs/Stores.md new file mode 100644 index 00000000..9c233706 --- /dev/null +++ b/src/hos_cosgi_testwebapp/webroot/libs/dstore/docs/Stores.md @@ -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()` in the query string. +* `selectParam` - This will specify the query parameter to use for specifying the `select` properties. This will default to `select()` in the query string. +* `rangeStartParam` and `rangeCountParam` - This will specify the query parameter to use for specifying the range. This will default to `limit(,)` 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. diff --git a/src/hos_cosgi_testwebapp/webroot/libs/dstore/docs/Testing.md b/src/hos_cosgi_testwebapp/webroot/libs/dstore/docs/Testing.md new file mode 100644 index 00000000..73795bf4 --- /dev/null +++ b/src/hos_cosgi_testwebapp/webroot/libs/dstore/docs/Testing.md @@ -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= +export SAUCE_ACCESS_KEY= +``` + +Then kick off the runner with the following command: + +``` +node node_modules/intern-geezer/runner config=tests/intern +``` diff --git a/src/hos_cosgi_testwebapp/webroot/libs/dstore/extensions/RqlQuery.js b/src/hos_cosgi_testwebapp/webroot/libs/dstore/extensions/RqlQuery.js new file mode 100644 index 00000000..c1f78807 --- /dev/null +++ b/src/hos_cosgi_testwebapp/webroot/libs/dstore/extensions/RqlQuery.js @@ -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); + } + }); +}); diff --git a/src/hos_cosgi_testwebapp/webroot/libs/dstore/legacy/DstoreAdapter.js b/src/hos_cosgi_testwebapp/webroot/libs/dstore/legacy/DstoreAdapter.js new file mode 100644 index 00000000..42bec8f5 --- /dev/null +++ b/src/hos_cosgi_testwebapp/webroot/libs/dstore/legacy/DstoreAdapter.js @@ -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); +}); diff --git a/src/hos_cosgi_testwebapp/webroot/libs/dstore/legacy/StoreAdapter.js b/src/hos_cosgi_testwebapp/webroot/libs/dstore/legacy/StoreAdapter.js new file mode 100644 index 00000000..f1146e77 --- /dev/null +++ b/src/hos_cosgi_testwebapp/webroot/libs/dstore/legacy/StoreAdapter.js @@ -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; + } + }); +}); diff --git a/src/hos_cosgi_testwebapp/webroot/libs/dstore/package.js b/src/hos_cosgi_testwebapp/webroot/libs/dstore/package.js new file mode 100644 index 00000000..727b7b91 --- /dev/null +++ b/src/hos_cosgi_testwebapp/webroot/libs/dstore/package.js @@ -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)); + } + } +}; \ No newline at end of file diff --git a/src/hos_cosgi_testwebapp/webroot/libs/dstore/package.json b/src/hos_cosgi_testwebapp/webroot/libs/dstore/package.json new file mode 100644 index 00000000..6c3fa9b7 --- /dev/null +++ b/src/hos_cosgi_testwebapp/webroot/libs/dstore/package.json @@ -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" +} diff --git a/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/Cache.js b/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/Cache.js new file mode 100644 index 00000000..3d9be099 --- /dev/null +++ b/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/Cache.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())); +}); diff --git a/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/Csv.js b/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/Csv.js new file mode 100644 index 00000000..30402853 --- /dev/null +++ b/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/Csv.js @@ -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'); + } + }); +}); \ No newline at end of file diff --git a/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/LocalDB.js b/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/LocalDB.js new file mode 100644 index 00000000..d4a9df79 --- /dev/null +++ b/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/LocalDB.js @@ -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'); + } + }; + } +}); diff --git a/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/LocalStorage.js b/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/LocalStorage.js new file mode 100644 index 00000000..0cd2d1c7 --- /dev/null +++ b/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/LocalStorage.js @@ -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])() + ]); + }); + } + + }; + } +}); diff --git a/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/Memory.js b/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/Memory.js new file mode 100644 index 00000000..41a8d436 --- /dev/null +++ b/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/Memory.js @@ -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(); + }) + }); +}); diff --git a/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/Request.js b/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/Request.js new file mode 100644 index 00000000..2b301da4 --- /dev/null +++ b/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/Request.js @@ -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; +}); diff --git a/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/RequestMemory.js b/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/RequestMemory.js new file mode 100644 index 00000000..f3a3cac7 --- /dev/null +++ b/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/RequestMemory.js @@ -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' ]); + }); + } + }); +}); diff --git a/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/Rest.js b/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/Rest.js new file mode 100644 index 00000000..6e7f8c87 --- /dev/null +++ b/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/Rest.js @@ -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); +}); diff --git a/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/SimpleQuery.js b/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/SimpleQuery.js new file mode 100644 index 00000000..bd23bf08 --- /dev/null +++ b/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/SimpleQuery.js @@ -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 } + ]); + } + }); +}); diff --git a/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/Store.js b/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/Store.js new file mode 100644 index 00000000..79bac006 --- /dev/null +++ b/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/Store.js @@ -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); + } + }); +}); diff --git a/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/Trackable.js b/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/Trackable.js new file mode 100644 index 00000000..0651435c --- /dev/null +++ b/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/Trackable.js @@ -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)); + })); +}); diff --git a/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/Tree.js b/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/Tree.js new file mode 100644 index 00000000..c32389f0 --- /dev/null +++ b/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/Tree.js @@ -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' } + ]); + } + }); +}); diff --git a/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/all.js b/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/all.js new file mode 100644 index 00000000..bb3c9c5a --- /dev/null +++ b/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/all.js @@ -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 () { +}); diff --git a/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/charting/StoreSeries.js b/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/charting/StoreSeries.js new file mode 100644 index 00000000..5710ef80 --- /dev/null +++ b/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/charting/StoreSeries.js @@ -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'); + } + }); +}); diff --git a/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/charting/stock.json b/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/charting/stock.json new file mode 100644 index 00000000..746d025e --- /dev/null +++ b/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/charting/stock.json @@ -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 } +]} diff --git a/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/charting/test_StoreSeries-amd.html b/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/charting/test_StoreSeries-amd.html new file mode 100644 index 00000000..d9499272 --- /dev/null +++ b/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/charting/test_StoreSeries-amd.html @@ -0,0 +1,184 @@ + + + + + +dstore StoreSeries Test + + + + + + + +

StoreSeries Test

+ +

+ Use the spinner fields at the bottom to change the data. The charts listen to store changes an update automatically. +

+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + + + + diff --git a/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/data/contributors.csv b/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/data/contributors.csv new file mode 100644 index 00000000..fbc0e29b --- /dev/null +++ b/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/data/contributors.csv @@ -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 \ No newline at end of file diff --git a/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/data/items.json b/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/data/items.json new file mode 100644 index 00000000..c48cafca --- /dev/null +++ b/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/data/items.json @@ -0,0 +1,6 @@ +[ + { "id": 1, "name": "foo" }, + { "id": 2, "name": "bar" }, + { "id": 3, "name": "baz" }, + { "id": 4, "name": "bim" } +] \ No newline at end of file diff --git a/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/data/node1.1 b/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/data/node1.1 new file mode 100644 index 00000000..b854b819 --- /dev/null +++ b/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/data/node1.1 @@ -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"} +]} diff --git a/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/data/node1.2 b/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/data/node1.2 new file mode 100644 index 00000000..541681da --- /dev/null +++ b/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/data/node1.2 @@ -0,0 +1 @@ +{ "id": "node1.2","name":"node1.2", "someProperty":"somePropertyA2"} diff --git a/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/data/noquote.csv b/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/data/noquote.csv new file mode 100644 index 00000000..b8a189f2 --- /dev/null +++ b/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/data/noquote.csv @@ -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 diff --git a/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/data/queryread.php b/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/data/queryread.php new file mode 100644 index 00000000..26aece8a --- /dev/null +++ b/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/data/queryread.php @@ -0,0 +1,47 @@ += $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. +} +?> \ No newline at end of file diff --git a/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/data/quote.csv b/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/data/quote.csv new file mode 100644 index 00000000..9dde51d1 --- /dev/null +++ b/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/data/quote.csv @@ -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! " diff --git a/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/data/testData.js b/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/data/testData.js new file mode 100644 index 00000000..ca0e1487 --- /dev/null +++ b/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/data/testData.js @@ -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'} + ] +}); diff --git a/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/data/treeTestRoot b/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/data/treeTestRoot new file mode 100644 index 00000000..874c5010 --- /dev/null +++ b/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/data/treeTestRoot @@ -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"} +] diff --git a/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/extensions/RqlQuery.js b/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/extensions/RqlQuery.js new file mode 100644 index 00000000..89f30a63 --- /dev/null +++ b/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/extensions/RqlQuery.js @@ -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); + } + }); + +}); diff --git a/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/intern.js b/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/intern.js new file mode 100644 index 00000000..c67d1238 --- /dev/null +++ b/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/intern.js @@ -0,0 +1,80 @@ +// Learn more about configuring this file at . +// 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?\// +}); diff --git a/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/legacy/DstoreAdapter-Memory.js b/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/legacy/DstoreAdapter-Memory.js new file mode 100644 index 00000000..b38e0e7a --- /dev/null +++ b/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/legacy/DstoreAdapter-Memory.js @@ -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 + }); + }) + }); +}); diff --git a/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/legacy/DstoreAdapter-Rest.js b/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/legacy/DstoreAdapter-Rest.js new file mode 100644 index 00000000..e627c271 --- /dev/null +++ b/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/legacy/DstoreAdapter-Rest.js @@ -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; + }); + } + }); +}); diff --git a/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/legacy/StoreAdapter-AsyncMemory.js b/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/legacy/StoreAdapter-AsyncMemory.js new file mode 100644 index 00000000..04481ae9 --- /dev/null +++ b/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/legacy/StoreAdapter-AsyncMemory.js @@ -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); + }); + } + }); +}); diff --git a/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/legacy/StoreAdapter-DojoData.js b/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/legacy/StoreAdapter-DojoData.js new file mode 100644 index 00000000..72955464 --- /dev/null +++ b/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/legacy/StoreAdapter-DojoData.js @@ -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); + } + })); +}); diff --git a/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/legacy/StoreAdapter-JsonRest.js b/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/legacy/StoreAdapter-JsonRest.js new file mode 100644 index 00000000..0901d4bd --- /dev/null +++ b/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/legacy/StoreAdapter-JsonRest.js @@ -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); + })); + } + }); +}); diff --git a/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/legacy/StoreAdapter-Memory.js b/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/legacy/StoreAdapter-Memory.js new file mode 100644 index 00000000..33b5f9f0 --- /dev/null +++ b/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/legacy/StoreAdapter-Memory.js @@ -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(); + }) + }); +}); diff --git a/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/mockRequest.js b/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/mockRequest.js new file mode 100644 index 00000000..d07faaff --- /dev/null +++ b/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/mockRequest.js @@ -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; +}); diff --git a/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/runTests.html b/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/runTests.html new file mode 100644 index 00000000..65c7bde1 --- /dev/null +++ b/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/runTests.html @@ -0,0 +1,9 @@ + + + + dstore Intern Unit Test Runner + + + Redirecting to Intern runner. + + diff --git a/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/sorting.js b/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/sorting.js new file mode 100644 index 00000000..21a62a7f --- /dev/null +++ b/src/hos_cosgi_testwebapp/webroot/libs/dstore/tests/sorting.js @@ -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); + } + }; + }; +}); \ No newline at end of file diff --git a/src/hos_cosgi_testwebapp/webroot/libs/dstore/typings/dstore.d.ts b/src/hos_cosgi_testwebapp/webroot/libs/dstore/typings/dstore.d.ts new file mode 100644 index 00000000..2eb7b679 --- /dev/null +++ b/src/hos_cosgi_testwebapp/webroot/libs/dstore/typings/dstore.d.ts @@ -0,0 +1,351 @@ +/// + +declare module dstore { + export interface FetchArray extends Array { + totalLength: number; + } + + export interface FetchPromise extends IPromise { + forEach(callback: (value: T, index?: number, array?: T[]) => void, thisObject?: any): FetchPromise; + response?: FetchResponse; + totalLength: IPromise; + } + + export interface FetchResponse { + data: T; + options: { [key: string]: any; }; + status?: number; + text: string; + url: string; + getHeader(name: string): string; + } + + export interface ChangeEvent { + id: any; + index?: number; + previousIndex?: number; + target: T; + totalLength: number; + type: string; + } + + export interface ICollection { + idProperty: string; + Model: { new (...args: any[]): T; }; + tracking?: { remove(): void; }; + + add(object: T, options?: {}): IPromise; + emit(eventName: string, event: ChangeEvent): boolean; + fetch(): dstore.FetchPromise; + fetchRange(kwArgs: { start?: number; end?: number; }): dstore.FetchPromise; + filter(query: string | {} | { (item: T, index: number): boolean; }): ICollection; + forEach(callback: (item: T, index: number) => void, thisObject?: any): IPromise; + get(id: any): IPromise; + getIdentity(object: T): any; + on(eventName: string, listener: (event: ChangeEvent) => void): IHandle; + put(object: T, options?: {}): IPromise; + remove(id: any): IPromise; + sort(property: string | { (a: T, b: T): number; }, descending?: boolean): ICollection; + track?(): ICollection; + } + + export interface ISyncCollection extends ICollection { + addSync(object: T, options?: {}): T; + fetchSync(): dstore.FetchArray; + fetchRangeSync(kwArgs: { start?: number; end?: number; }): dstore.FetchArray; + filter(query: string | {} | { (item: T, index: number): boolean; }): ISyncCollection; + getSync(id: any): T; + putSync(object: T, options?: {}): T; + removeSync(id: any): boolean; + sort(property: string | { (a: T, b: T): number; }, descending?: boolean): ISyncCollection; + track?(): ISyncCollection; + } +} + +declare module 'dstore/Csv' { + interface IStringifyOptions { + alwaysQuote: boolean; + trailingNewline: boolean; + } + + class Csv { + 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 extends Store { + cachingStore: dstore.ICollection; + evict(id: any): void; + invalidate(): void; + isAvailableInCache(): void; + } + + export = Cache; +} + +declare module 'dstore/legacy/StoreAdapter' { + import Store = require('dstore/Store'); + + class StoreAdapter extends Store {} + + export = StoreAdapter; +} + +declare module 'dstore/legacy/DstoreAdapter' { + class DstoreAdapter { + constructor(collection: dstore.ICollection); + 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 extends Store implements dstore.ISyncCollection { + data: T[]; + + constructor(kwArgs?: Memory.KwArgs); + + addSync(object: T, options?: {}): T; + fetchSync(): dstore.FetchArray; + fetchRangeSync(kwArgs: { start?: number; end?: number; }): dstore.FetchArray; + filter(query: string | {} | { (item: T, index: number): boolean; }): Memory; + 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; + track(): Memory; + } + + module Memory { + export interface KwArgs extends Store.KwArgs { + data?: T[]; + } + } + + export = Memory; +} + +declare module 'dstore/charting/StoreSeries' { + class StoreSeries { + constructor(collection: dstore.ICollection | dstore.ISyncCollection, value: any); + destroy(): void; + setSeriesObject(series: any): void; + fetch(): void; + } + + export = StoreSeries; +} + +declare module 'dstore/Tree' { + import Store = require('dstore/Store'); + + class Tree extends Store { + mayHaveChildren(parent: T): boolean; + getRootCollection(): Tree; + getChildren(parent: T): Tree; + } + + export = Tree; +} + +declare module 'dstore/SimpleQuery' { + import Store = require('dstore/Store'); + + class SimpleQuery extends Store {} + + export = SimpleQuery; +} + +declare module 'dstore/extensions/RqlQuery' { + import SimpleQuery = require('dstore/SimpleQuery'); + + class RqlQuery extends SimpleQuery {} + + export = RqlQuery; +} + +declare module 'dstore/LocalDB' { + import Store = require('dstore/Store'); + + class LocalDB extends Store { + constructor(kwArgs: LocalDB.KwArgs); + 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 extends Store.KwArgs { + dbConfig: IDBConfig; + storeName: string; + } + } + + export = LocalDB; +} + +declare module 'dstore/db/IndexedDB' { + import LocalDB = require('dstore/LocalDB'); + + class IndexedDB extends LocalDB {} + + export = IndexedDB; +} + +declare module 'dstore/db/SQL' { + import LocalDB = require('dstore/LocalDB'); + + class SQL extends LocalDB {} + + export = SQL; +} + +declare module 'dstore/db/LocalStorage' { + import LocalDB = require('dstore/LocalDB'); + + class LocalStorageDB extends LocalDB {} + + export = LocalStorageDB; +} + +declare module 'dstore/Trackable' { + class Trackable { + currentRange: any[]; + track(): dstore.ICollection; + } + + export = Trackable; +} + +declare module 'dstore/Request' { + import Store = require('dstore/Store'); + + class Request extends Store { + 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; + sort(property: string | { (a: T, b: T): number; }, descending?: boolean): Request; + track(): Request; + } + + 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 extends Request implements Cache { + cachingStore: dstore.ICollection; + evict(id: any): void; + + filter(query: string | {} | { (item: T, index: number): boolean; }): RequestMemory; + invalidate(): void; + isAvailableInCache(): void; + sort(property: string | { (a: T, b: T): number; }, descending?: boolean): RequestMemory; + track(): RequestMemory; + } + + export = RequestMemory; +} + +declare module 'dstore/Rest' { + import Request = require('dstore/Request'); + + class Rest extends Request { + filter(query: string | {} | { (item: T, index: number): boolean; }): Rest; + sort(property: string | { (a: T, b: T): number; }, descending?: boolean): Rest; + track(): Rest; + } + + export = Rest; +} + +declare module 'dstore/Store' { + import decl = require('dojo/_base/declare'); + class Store extends decl.Base implements dstore.ICollection { + idProperty: string; + Model: { new (...args: any[]): T; }; + total: IPromise; + + constructor(kwArgs?: Store.KwArgs); + + add(object: T, options?: {}): IPromise; + emit(eventName: string, event: dstore.ChangeEvent): boolean; + fetch(): dstore.FetchPromise; + fetchRange(kwArgs: { start?: number; end?: number; }): dstore.FetchPromise; + filter(query: string | {} | { (item: T, index: number): boolean; }): Store; + forEach(callback: (item: T, index: number) => void, thisObject?: any): IPromise; + get(id: any): IPromise; + getIdentity(object: T): any; + on(eventName: string, listener: (event: dstore.ChangeEvent) => void): IHandle; + put(object: T, options?: {}): IPromise; + remove(id: any): IPromise<{}>; + sort(property: string | { (a: T, b: T): number; }, descending?: boolean): Store; + } + + module Store { + export interface KwArgs { + idProperty?: typeof Store.prototype.idProperty; + Model?: typeof Store.prototype.Model; + } + } + + export = Store; +} diff --git a/src/hos_cosgi_webserver/include/hos_cosgi_webserver/hos/cosgi/service/webserver/webresource/WebResourceHandlerImpl.h b/src/hos_cosgi_webserver/include/hos_cosgi_webserver/hos/cosgi/service/webserver/webresource/WebResourceHandlerImpl.h index 373158ad..3cb29986 100644 --- a/src/hos_cosgi_webserver/include/hos_cosgi_webserver/hos/cosgi/service/webserver/webresource/WebResourceHandlerImpl.h +++ b/src/hos_cosgi_webserver/include/hos_cosgi_webserver/hos/cosgi/service/webserver/webresource/WebResourceHandlerImpl.h @@ -10,8 +10,16 @@ #include #include #include +#include #include +#include +#include #include +#include +#include +#include +#include +#include @@ -41,6 +49,211 @@ public: }; + +template +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 _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 lock(_mutex); + _eventQueue.push(event); + } + _cv.notify_one(); + } + + libvariant::Variant popEvent() { + std::unique_lock 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 + 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 WebResourceHandlerFactoryImpl; + + + +template +class WebResourceHandlerFactoryImpl + : public virtual webserver::WebResourceHandlerFactory, + public implementation::Service { +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 WebResourceHandlerFactoryImpl + : public virtual webserver::WebResourceHandlerFactory, + public implementation::Service { +private: + webserver::WebServerService* service = nullptr; + boost::asio::io_service io_service; + std::unique_ptr io_service_thread; + std::map> 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 + std::shared_ptr getEventQueue( const std::string& clientID, Args... args) { + if (clientID.empty()) { + return std::make_shared(clientID, static_cast(this), args...); + } else { + auto i = eventQueues.find(clientID); + if (i == eventQueues.end()) { + auto q = std::make_shared(clientID, static_cast(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 { + : public WebResourceHandlerFactoryImpl { public: typedef std::function 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, diff --git a/src/hos_cosgi_webserver/src/hos/cosgi/service/webserver/WebServerServiceImpl.cpp b/src/hos_cosgi_webserver/src/hos/cosgi/service/webserver/WebServerServiceImpl.cpp index 1d095fee..17e22421 100644 --- a/src/hos_cosgi_webserver/src/hos/cosgi/service/webserver/WebServerServiceImpl.cpp +++ b/src/hos_cosgi_webserver/src/hos/cosgi/service/webserver/WebServerServiceImpl.cpp @@ -7,6 +7,8 @@ #include #include #include +#include +#include #include #include "webresource/WebSocketTest.h"