├── .eslintignore ├── .github └── browserstack.png ├── deploy.sh ├── src └── Database │ └── IndexedDB │ ├── IDBKey.purs │ ├── IDBKeyRange.js │ ├── IDBKey │ ├── Internal.js │ └── Internal.purs │ ├── Core.js │ ├── IDBFactory.js │ ├── IDBTransaction.js │ ├── IDBFactory.purs │ ├── IDBTransaction.purs │ ├── IDBCursor.js │ ├── IDBKeyRange.purs │ ├── IDBDatabase.js │ ├── IDBObjectStore.js │ ├── IDBIndex.js │ ├── IDBCursor.purs │ ├── IDBDatabase.purs │ ├── IDBIndex.purs │ ├── IDBObjectStore.purs │ └── Core.purs ├── .eslintrc ├── karma.conf.js ├── .travis.yml ├── LICENSE ├── bower.json ├── .gitignore ├── package.json ├── karma.browserstack.conf.js ├── README.md └── test └── Main.purs /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/* 2 | -------------------------------------------------------------------------------- /.github/browserstack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/truqu/purescript-indexedDB/HEAD/.github/browserstack.png -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | git stash save 4 | git clean -df 5 | yes | pulp publish --no-push 6 | git stash pop 7 | -------------------------------------------------------------------------------- /src/Database/IndexedDB/IDBKey.purs: -------------------------------------------------------------------------------- 1 | -- | A key has an associated type which is one of: number, date, string, or array. 2 | -- | 3 | -- | NOTE: Binary keys aren't supported yet. 4 | module Database.IndexedDB.IDBKey 5 | ( module Database.IndexedDB.IDBKey.Internal 6 | ) where 7 | 8 | import Database.IndexedDB.IDBKey.Internal 9 | ( class IDBKey 10 | , Key 11 | , none 12 | , toKey 13 | , fromKey 14 | , unsafeFromKey 15 | ) 16 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "env": { 4 | "commonjs": true 5 | }, 6 | "parserOptions": { 7 | "ecmaVersion": 5 8 | }, 9 | "globals": { 10 | "indexedDB": true, 11 | "IDBKeyRange": true, 12 | "PS": true 13 | }, 14 | "rules": { 15 | "import/no-extraneous-dependencies": 0, 16 | "import/no-unresolved": 0, 17 | "indent": ["error", 4], 18 | "no-param-reassign": 0, 19 | "no-underscore-dangle": 0, 20 | "no-var": 0, 21 | "object-shorthand": 0, 22 | "prefer-template": 0 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = config => { 2 | config.set({ 3 | autoWatch: true, 4 | singleRun: true, 5 | browsers: ["Chrome", "Firefox"], 6 | files: [ 7 | "dist/karma/index.js", 8 | ], 9 | frameworks: [ 10 | "mocha", 11 | ], 12 | plugins: [ 13 | "karma-chrome-launcher", 14 | "karma-firefox-launcher", 15 | "karma-spec-reporter", 16 | "karma-mocha", 17 | ], 18 | reporters: ["spec"], 19 | client: { 20 | mocha: { 21 | timeout: 10000 22 | } 23 | } 24 | }); 25 | }; 26 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - node 4 | 5 | addons: 6 | browserstack: 7 | username: $BROWSERSTACK_USERNAME 8 | access_key: $BROWSERSTACK_ACCESSKEY 9 | 10 | before_install: 11 | - export PATH=./node_modules/.bin:$PATH 12 | 13 | install: 14 | - npm install 15 | - bower install 16 | 17 | script: 18 | - npm run test:browserstack 19 | 20 | after_script: 21 | - npm run prepare:release 22 | 23 | deploy: 24 | - provider: releases 25 | api_key: $GITHUB_TOKEN 26 | skip_cleanup: true 27 | file_glob: true 28 | file: releases/github/* 29 | on: 30 | tags: true 31 | branch: master 32 | 33 | - provider: script 34 | script: deploy.sh 35 | on: 36 | tags: true 37 | branch: master 38 | -------------------------------------------------------------------------------- /src/Database/IndexedDB/IDBKeyRange.js: -------------------------------------------------------------------------------- 1 | exports._bound = function _bound(lower, upper, lowerOpen, upperOpen) { 2 | try { 3 | return IDBKeyRange.bound(lower, upper, lowerOpen, upperOpen); 4 | } catch (e) { 5 | return null; 6 | } 7 | }; 8 | 9 | exports._includes = function _includes(range, key) { 10 | return range.includes(key); 11 | }; 12 | 13 | exports._lower = function _lower(range) { 14 | return range.lower; 15 | }; 16 | 17 | exports._lowerBound = function _lowerBound(lower, open) { 18 | return IDBKeyRange.lowerBound(lower, open); 19 | }; 20 | 21 | exports._lowerOpen = function _lowerOpen(range) { 22 | return range.lowerOpen; 23 | }; 24 | 25 | exports._only = function _only(key) { 26 | return IDBKeyRange.only(key); 27 | }; 28 | 29 | exports._upper = function _upper(range) { 30 | return range.upper; 31 | }; 32 | 33 | exports._upperBound = function _upperBound(upper, open) { 34 | return IDBKeyRange.upperBound(upper, open); 35 | }; 36 | 37 | exports._upperOpen = function _upperOpen(range) { 38 | return range.upperOpen; 39 | }; 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 TruQu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Database/IndexedDB/IDBKey/Internal.js: -------------------------------------------------------------------------------- 1 | exports._dateTimeToForeign = function _dateTimeToForeign(y, m, d, h, mi, s, ms) { 2 | return new Date(y, m, d, h, mi, s, ms); 3 | }; 4 | 5 | exports._readDateTime = function _readDateTime(parse, right, left, date) { 6 | if (Object.getPrototypeOf(date) !== Date.prototype) { 7 | return left(typeof date); 8 | } 9 | 10 | const y = date.getFullYear(); 11 | const m = date.getMonth() + 1; 12 | const d = date.getDate(); 13 | const h = date.getHours(); 14 | const mi = date.getMinutes(); 15 | const s = date.getSeconds(); 16 | const ms = date.getMilliseconds(); 17 | 18 | const mdate = parse(y)(m)(d)(h)(mi)(s)(ms); 19 | 20 | if (mdate == null) { 21 | return left(typeof date); // TODO Could return better error 22 | } 23 | 24 | return right(mdate); 25 | }; 26 | 27 | exports._unsafeReadDateTime = function _unsafeReadDateTime(parse, date) { 28 | const y = date.getFullYear(); 29 | const m = date.getMonth() + 1; 30 | const d = date.getDate(); 31 | const h = date.getHours(); 32 | const mi = date.getMinutes(); 33 | const s = date.getSeconds(); 34 | const ms = date.getMilliseconds(); 35 | 36 | return parse(y)(m)(d)(h)(mi)(s)(ms); 37 | }; 38 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "purescript-indexeddb", 3 | "description": "An API wrapper around IndexedDB", 4 | "version": "4.0.0", 5 | "authors": [ 6 | "Matthias Benkort " 7 | ], 8 | "ignore": [ 9 | "**/.*", 10 | "node_modules", 11 | "bower_components", 12 | "output", 13 | "test", 14 | "dist", 15 | "releases", 16 | "package.json", 17 | "karma.conf.js", 18 | "karma.browserstack.conf.js", 19 | "deploy.sh" 20 | ], 21 | "license": "MIT", 22 | "homepage": "https://github.com/truqu/purescript-indexedDB", 23 | "repository": { 24 | "type": "git", 25 | "url": "git://github.com/truqu/purescript-indexedDB.git" 26 | }, 27 | "dependencies": { 28 | "purescript-aff": "^4.0.2", 29 | "purescript-datetime": "^3.2.0", 30 | "purescript-eff": "^3.1.0", 31 | "purescript-exceptions": "^3.0.0", 32 | "purescript-foreign": "^4.0.1", 33 | "purescript-maybe": "^3.0.0", 34 | "purescript-nullable": "^3.0.0", 35 | "purescript-prelude": "^3.1.0", 36 | "purescript-read": "^1.0.0" 37 | }, 38 | "devDependencies": { 39 | "purescript-now": "^3.0.0", 40 | "purescript-psci-support": "^3.0.0", 41 | "purescript-spec": "^2.0.0", 42 | "purescript-spec-mocha": "^2.0.0" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Bower ### 2 | bower_components 3 | .bower-cache 4 | .bower-registry 5 | .bower-tmp 6 | 7 | ### Node ### 8 | # Logs 9 | logs 10 | *.log 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | 15 | # Runtime data 16 | pids 17 | *.pid 18 | *.seed 19 | *.pid.lock 20 | 21 | # Directory for instrumented libs generated by jscoverage/JSCover 22 | lib-cov 23 | 24 | # Coverage directory used by tools like istanbul 25 | coverage 26 | 27 | # nyc test coverage 28 | .nyc_output 29 | 30 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 31 | .grunt 32 | 33 | # Bower dependency directory (https://bower.io/) 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (http://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Typescript v1 declaration files 46 | typings/ 47 | 48 | # Optional npm cache directory 49 | .npm 50 | 51 | # Optional eslint cache 52 | .eslintcache 53 | 54 | # Optional REPL history 55 | .node_repl_history 56 | 57 | # Output of 'npm pack' 58 | *.tgz 59 | 60 | # Yarn Integrity file 61 | .yarn-integrity 62 | 63 | # dotenv environment variables file 64 | .env 65 | 66 | 67 | ### PureScript ### 68 | # Dependencies 69 | .psci_modules 70 | 71 | # Generated files 72 | output 73 | generated-docs 74 | releases 75 | .pulp-cache 76 | .psc* 77 | .purs* 78 | .psa* 79 | dist 80 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "purescript-indexeddb", 3 | "version": "3.0.0", 4 | "description": "A wrapper around the IndexedDB API", 5 | "scripts": { 6 | "prepare:release": "mkdir -p releases/github && cp -r README.md LICENSE src bower.json releases/github", 7 | "test:build": "mkdir -p dist/karma && pulp browserify -j 4 --main \"Test.Main\" -I test --to dist/karma/index.js", 8 | "test:run": "karma start karma.conf.js", 9 | "test:run:browserstack": "karma start karma.browserstack.conf.js", 10 | "test:browserstack": "npm run test:build && npm run test:run:browserstack", 11 | "test": "npm run test:build && npm run test:run", 12 | "start": "find . -type f -regex '.*\\(src\\|test\\).*' ! -regex '.*bower_components.*\\|.*node_modules.*|.*\\.sw.*' | entr -s 'npm run test -s'" 13 | }, 14 | "dependencies": {}, 15 | "devDependencies": { 16 | "bower": "1.8.0", 17 | "eslint": "3.19.0", 18 | "eslint-config-airbnb": "15.0.1", 19 | "eslint-plugin-import": "2.5.0", 20 | "eslint-plugin-jsx-a11y": "5.0.3", 21 | "eslint-plugin-react": "7.1.0", 22 | "karma": "1.7.0", 23 | "karma-browserstack-launcher": "1.3.0", 24 | "karma-chrome-launcher": "2.1.1", 25 | "karma-firefox-launcher": "1.0.1", 26 | "karma-mocha": "1.3.0", 27 | "karma-spec-reporter": "0.0.31", 28 | "mocha": "3.4.2", 29 | "pulp": "11.0.0", 30 | "purescript": "0.11.5" 31 | }, 32 | "repository": { 33 | "type": "git", 34 | "url": "git+https://github.com/Truqu/purescript-indexeddb.git" 35 | }, 36 | "author": "Matthias Benkort ", 37 | "license": "MIT", 38 | "bugs": { 39 | "url": "https://github.com/Truqu/purescript-indexeddb/issues" 40 | }, 41 | "homepage": "https://github.com/Truqu/purescript-indexeddb#readme" 42 | } 43 | -------------------------------------------------------------------------------- /src/Database/IndexedDB/Core.js: -------------------------------------------------------------------------------- 1 | const toArray = function toArray(xs) { 2 | return Array.prototype.slice.apply(xs); 3 | }; 4 | 5 | 6 | exports._showCursor = function _showCursor(cursor) { 7 | return '(IDBCursor ' + 8 | '{ direction: ' + cursor.direction + 9 | ', key: ' + cursor.key + 10 | ', primaryKey: ' + cursor.primaryKey + 11 | ' })'; 12 | }; 13 | 14 | exports._showDatabase = function _showDatabase(db) { 15 | return '(IDBDatabase ' + 16 | '{ name: ' + db.name + 17 | ', objectStoreNames: [' + toArray(db.objectStoreNames).join(', ') + ']' + 18 | ', version: ' + db.version + 19 | ' })'; 20 | }; 21 | 22 | exports._showIndex = function _showIndex(index) { 23 | return '(IDBIndex ' + 24 | '{ name: ' + index.name + 25 | ', keyPath: ' + index.keyPath + 26 | ', multiEntry: ' + index.multiEntry + 27 | ', unique: ' + index.unique + 28 | ' })'; 29 | }; 30 | 31 | exports._showKeyRange = function _showKeyRange(range) { 32 | return '(IDBKeyRange ' + 33 | '{ lower: ' + range.lower + 34 | ', upper: ' + range.upper + 35 | ', lowerOpen: ' + range.lowerOpen + 36 | ', upperOpen: ' + range.upperOpen + 37 | ' })'; 38 | }; 39 | 40 | exports._showObjectStore = function _showObjectStore(store) { 41 | return '(IDBObjectStore ' + 42 | '{ autoIncrement: ' + store.autoIncrement + 43 | ', indexNames: [' + toArray(store.indexNames).join(', ') + ']' + 44 | ', keyPath: ' + store.keyPath + 45 | ', name: ' + store.name + 46 | ' })'; 47 | }; 48 | 49 | exports._showTransaction = function _showTransaction(tx) { 50 | return '(IDBTransaction ' + 51 | '{ error: ' + tx.error + 52 | ', mode: ' + tx.mode + 53 | ' })'; 54 | }; 55 | -------------------------------------------------------------------------------- /src/Database/IndexedDB/IDBFactory.js: -------------------------------------------------------------------------------- 1 | const errorHandler = function errorHandler(cb) { 2 | return function _handler(e) { 3 | cb(e.target.error); 4 | }; 5 | }; 6 | 7 | const noOp = function noOp() { 8 | return function eff() { 9 | // Nothing 10 | }; 11 | }; 12 | 13 | const noOp2 = function noOp2() { 14 | return noOp; 15 | }; 16 | 17 | const noOp3 = function noOp3() { 18 | return noOp2; 19 | }; 20 | 21 | exports._deleteDatabase = function _deleteDatabase(name) { 22 | return function aff(error, success) { 23 | try { 24 | const request = indexedDB.deleteDatabase(name); 25 | 26 | request.onsuccess = function onSuccess(e) { 27 | success(e.oldVersion); 28 | }; 29 | 30 | request.onerror = errorHandler(error); 31 | } catch (e) { 32 | error(e); 33 | } 34 | 35 | return function canceler(_, cancelerError) { 36 | cancelerError(new Error("Can't cancel IDB Effects")); 37 | }; 38 | }; 39 | }; 40 | 41 | exports._open = function _open(fromMaybe, name, mver, req) { 42 | const ver = fromMaybe(undefined)(mver); 43 | 44 | return function aff(error, success) { 45 | try { 46 | const request = indexedDB.open(name, ver); 47 | request.onsuccess = function onSuccess(e) { 48 | success(e.target.result); 49 | }; 50 | 51 | request.onblocked = function onBlocked() { 52 | fromMaybe(noOp)(req.onBlocked)(); 53 | }; 54 | 55 | request.onupgradeneeded = function onUpgradeNeeded(e) { 56 | const meta = { oldVersion: e.oldVersion }; 57 | // eslint-disable-next-line max-len 58 | fromMaybe(noOp3)(req.onUpgradeNeeded)(e.target.result)(e.target.transaction)(meta)(); 59 | }; 60 | 61 | request.onerror = errorHandler(error); 62 | } catch (e) { 63 | error(e); 64 | } 65 | 66 | return function canceler(_, cancelerError) { 67 | cancelerError(new Error("Can't cancel IDB Effects")); 68 | }; 69 | }; 70 | }; 71 | -------------------------------------------------------------------------------- /src/Database/IndexedDB/IDBTransaction.js: -------------------------------------------------------------------------------- 1 | exports._abort = function _abort(tx) { 2 | return function aff(error, success) { 3 | try { 4 | tx.abort(); 5 | success(); 6 | } catch (e) { 7 | error(e); 8 | } 9 | 10 | return function canceler(_, cancelerError) { 11 | cancelerError(new Error("Can't cancel IDB Effects")); 12 | }; 13 | }; 14 | }; 15 | 16 | exports._db = function _db(tx) { 17 | return tx.db; 18 | }; 19 | 20 | exports._error = function _error(tx) { 21 | return tx.error == null 22 | ? null 23 | : tx.error; 24 | }; 25 | 26 | exports._mode = function _mode(ReadOnly, ReadWrite, VersionChange, tx) { 27 | if (tx.mode === 'readwrite') { 28 | return ReadWrite; 29 | } else if (tx.mode === 'versionchange') { 30 | return VersionChange; 31 | } 32 | 33 | return ReadOnly; 34 | }; 35 | 36 | exports._objectStore = function _objectStore(tx, name) { 37 | return function aff(error, success) { 38 | try { 39 | const store = tx.objectStore(name); 40 | success(store); 41 | } catch (e) { 42 | error(e); 43 | } 44 | 45 | return function canceler(_, cancelerError) { 46 | cancelerError(new Error("Can't cancel IDB Effects")); 47 | }; 48 | }; 49 | }; 50 | 51 | exports._objectStoreNames = function _objectStoreNames(tx) { 52 | return tx.objectStoreNames; 53 | }; 54 | 55 | exports._onAbort = function _onAbort(tx, f) { 56 | return function aff(error, success) { 57 | tx.onabort = function onabort() { 58 | f(); 59 | }; 60 | success(); 61 | 62 | return function canceler(_, cancelerError) { 63 | cancelerError(new Error("Can't cancel IDB Effects")); 64 | }; 65 | }; 66 | }; 67 | 68 | exports._onComplete = function _onComplete(tx, f) { 69 | return function aff(error, success) { 70 | tx.oncomplete = function oncomplete() { 71 | f(); 72 | }; 73 | success(); 74 | 75 | return function canceler(_, cancelerError) { 76 | cancelerError(new Error("Can't cancel IDB Effects")); 77 | }; 78 | }; 79 | }; 80 | 81 | exports._onError = function _onError(tx, f) { 82 | return function aff(error, success) { 83 | tx.onerror = function onerror(e) { 84 | f(e.target.error)(); 85 | }; 86 | success(); 87 | 88 | return function canceler(_, cancelerError) { 89 | cancelerError(new Error("Can't cancel IDB Effects")); 90 | }; 91 | }; 92 | }; 93 | -------------------------------------------------------------------------------- /karma.browserstack.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = (config) => { 2 | config.set({ 3 | browsers: [ 4 | 'FirefoxMAC', 5 | 'ChromeMAC', 6 | // 'SafariMAC', 7 | 'OperaMAC', 8 | 9 | 'FirefoxWIN', 10 | 'ChromeWIN', 11 | 'OperaWIN', 12 | ], 13 | files: [ 14 | 'dist/karma/index.js', 15 | ], 16 | frameworks: [ 17 | 'mocha', 18 | ], 19 | plugins: [ 20 | 'karma-browserstack-launcher', 21 | 'karma-mocha', 22 | ], 23 | reporters: [ 24 | 'dots', 25 | 'BrowserStack', 26 | ], 27 | singleRun: true, 28 | client: { 29 | mocha: { 30 | timeout: 10000, 31 | }, 32 | }, 33 | browserStack: { 34 | username: process.env.BROWSERSTACK_USERNAME, 35 | accessKey: process.env.BROWSERSTACK_ACCESSKEY, 36 | timeout: 1500, 37 | captureTimeout: 500, 38 | }, 39 | customLaunchers: { 40 | FirefoxMAC: { 41 | base: 'BrowserStack', 42 | browser: 'Firefox', 43 | browser_version: '51', 44 | os: 'OS X', 45 | os_version: 'Sierra', 46 | }, 47 | ChromeMAC: { 48 | base: 'BrowserStack', 49 | browser: 'Chrome', 50 | browser_version: '58', 51 | os: 'OS X', 52 | os_version: 'Sierra', 53 | }, 54 | SafariMAC: { 55 | base: 'BrowserStack', 56 | browser: 'Safari', 57 | browser_version: '10.1', 58 | os: 'OS X', 59 | os_version: 'Sierra', 60 | }, 61 | OperaMAC: { 62 | base: 'BrowserStack', 63 | browser: 'Opera', 64 | browser_version: '46', 65 | os: 'OS X', 66 | os_version: 'Sierra', 67 | }, 68 | FirefoxWIN: { 69 | base: 'BrowserStack', 70 | browser: 'Firefox', 71 | browser_version: '51', 72 | os: 'WINDOWS', 73 | os_version: '10', 74 | }, 75 | ChromeWIN: { 76 | base: 'BrowserStack', 77 | browser: 'Chrome', 78 | browser_version: '58', 79 | os: 'WINDOWS', 80 | os_version: '10', 81 | }, 82 | OperaWIN: { 83 | base: 'BrowserStack', 84 | browser: 'Opera', 85 | browser_version: '46', 86 | os: 'WINDOWS', 87 | os_version: '10', 88 | }, 89 | }, 90 | }); 91 | }; 92 | -------------------------------------------------------------------------------- /src/Database/IndexedDB/IDBFactory.purs: -------------------------------------------------------------------------------- 1 | -- | Database objects are accessed through methods on the IDBFactory interface. 2 | -- | A single object implementing this interface is present in the global scope 3 | -- | of environments that support Indexed DB operations. 4 | module Database.IndexedDB.IDBFactory 5 | -- * Types 6 | ( Callbacks 7 | , DatabaseName 8 | , Version 9 | 10 | -- * Interface 11 | , deleteDatabase 12 | , open 13 | ) where 14 | 15 | import Prelude (Unit, ($), (<<<)) 16 | 17 | import Control.Monad.Aff (Aff) 18 | import Control.Monad.Aff.Compat (fromEffFnAff, EffFnAff) 19 | import Control.Monad.Eff (Eff) 20 | import Data.Function.Uncurried as Fn 21 | import Data.Function.Uncurried (Fn4) 22 | import Data.Maybe (Maybe, fromMaybe) 23 | 24 | import Database.IndexedDB.Core 25 | 26 | 27 | -------------------- 28 | -- TYPES 29 | -- 30 | 31 | -- Type alias for binding listeners to an initial open action. 32 | type Callbacks e = 33 | { onBlocked :: Maybe (Eff (| e) Unit) 34 | , onUpgradeNeeded :: Maybe (Database -> Transaction -> { oldVersion :: Int } -> Eff (| e) Unit) 35 | } 36 | 37 | 38 | -- | Type alias for DatabaseName. 39 | type DatabaseName = String 40 | 41 | 42 | -- | Type alias for Version. 43 | type Version = Int 44 | 45 | 46 | -------------------- 47 | -- INTERFACE 48 | -- 49 | 50 | -- | Attempts to delete the named database. If the database already exists 51 | -- | and there are open connections that don’t close in response to a 52 | -- | `versionchange` event, the request will be blocked until all they close. 53 | deleteDatabase 54 | :: forall e 55 | . DatabaseName 56 | -> Aff (idb :: IDB | e) Int 57 | deleteDatabase = 58 | fromEffFnAff <<< _deleteDatabase 59 | 60 | 61 | -- | Attempts to open a connection to the named database with the specified version. 62 | -- | If the database already exists with a lower version and there are open connections 63 | -- | that don’t close in response to a versionchange event, the request will be blocked 64 | -- | until all they close, then an upgrade will occur. If the database already exists with 65 | -- | a higher version the request will fail. 66 | -- | 67 | -- | When the version isn't provided (`Nothing`), attempts to open a connection to the 68 | -- | named database with the current version, or 1 if it does not already exist. 69 | open 70 | :: forall e e' 71 | . DatabaseName 72 | -> Maybe Version 73 | -> Callbacks e' 74 | -> Aff (idb :: IDB | e) Database 75 | open name mver req = 76 | fromEffFnAff $ Fn.runFn4 _open fromMaybe name mver req 77 | 78 | 79 | -------------------- 80 | -- FFI 81 | -- 82 | foreign import _deleteDatabase 83 | :: forall e 84 | . String 85 | -> EffFnAff (idb :: IDB | e) Int 86 | 87 | 88 | foreign import _open 89 | :: forall a e e' 90 | . Fn4 (a -> Maybe a -> a) String (Maybe Int) (Callbacks e') (EffFnAff (idb :: IDB | e) Database) 91 | -------------------------------------------------------------------------------- /src/Database/IndexedDB/IDBTransaction.purs: -------------------------------------------------------------------------------- 1 | -- | An object store is the primary storage mechanism for storing data in a database. 2 | module Database.IndexedDB.IDBTransaction 3 | -- * Interface 4 | ( abort 5 | , objectStore 6 | 7 | -- * Attributes 8 | , error 9 | , mode 10 | 11 | -- * Event Handlers 12 | , onAbort 13 | , onComplete 14 | , onError 15 | ) where 16 | 17 | import Prelude (Unit, ($), (<<<), (>>>)) 18 | 19 | import Control.Monad.Aff (Aff) 20 | import Control.Monad.Aff.Compat (EffFnAff, fromEffFnAff) 21 | import Control.Monad.Eff (Eff) 22 | import Control.Monad.Eff.Exception (Error) 23 | import Data.Function.Uncurried as Fn 24 | import Data.Function.Uncurried (Fn2, Fn4) 25 | import Data.Maybe (Maybe) 26 | import Data.Nullable (Nullable, toMaybe) 27 | 28 | import Database.IndexedDB.Core 29 | 30 | 31 | -------------------- 32 | -- INTERFACES 33 | -- 34 | 35 | -- | Aborts the transaction. All pending requests will fail with a "AbortError" 36 | -- | DOMException and all changes made to the database will be reverted. 37 | abort 38 | :: forall e tx. (IDBTransaction tx) 39 | => tx 40 | -> Aff (idb :: IDB | e) Unit 41 | abort = 42 | fromEffFnAff <<< _abort 43 | 44 | -- | Returns an IDBObjectStore in the transaction's scope. 45 | objectStore 46 | :: forall e tx. (IDBTransaction tx) 47 | => tx 48 | -> String 49 | -> Aff (idb :: IDB | e) ObjectStore 50 | objectStore tx name = 51 | fromEffFnAff $ Fn.runFn2 _objectStore tx name 52 | 53 | 54 | -------------------- 55 | -- ATTRIBUTES 56 | -- 57 | 58 | --- | Returns the transaction’s connection. 59 | db 60 | :: Transaction 61 | -> Database 62 | db = 63 | _db 64 | 65 | 66 | -- | If the transaction was aborted, returns the error (a DOMException) providing the reason. 67 | error 68 | :: Transaction 69 | -> Maybe Error 70 | error = 71 | _error >>> toMaybe 72 | 73 | 74 | -- | Returns the mode the transaction was created with (`ReadOnly|ReadWrite`) 75 | -- | , or `VersionChange` for an upgrade transaction. 76 | mode 77 | :: Transaction 78 | -> TransactionMode 79 | mode = 80 | Fn.runFn4 _mode ReadOnly ReadWrite VersionChange 81 | 82 | 83 | -- | Returns a list of the names of object stores in the transaction’s scope. 84 | -- | For an upgrade transaction this is all object stores in the database. 85 | objectStoreNames 86 | :: Transaction 87 | -> Array String 88 | objectStoreNames = 89 | _objectStoreNames 90 | 91 | 92 | -------------------- 93 | -- EVENT HANDLERS 94 | -- 95 | 96 | -- | Event handler for the `abort` event. 97 | onAbort 98 | :: forall e e' 99 | . Transaction 100 | -> Eff ( | e') Unit 101 | -> Aff (idb :: IDB | e) Unit 102 | onAbort db' f = 103 | fromEffFnAff $ Fn.runFn2 _onAbort db' f 104 | 105 | 106 | -- | Event handler for the `complete` event. 107 | onComplete 108 | :: forall e e' 109 | . Transaction 110 | -> Eff ( | e') Unit 111 | -> Aff (idb :: IDB | e) Unit 112 | onComplete db' f = 113 | fromEffFnAff $ Fn.runFn2 _onComplete db' f 114 | 115 | 116 | -- | Event handler for the `error` event. 117 | onError 118 | :: forall e e' 119 | . Transaction 120 | -> (Error -> Eff ( | e') Unit) 121 | -> Aff (idb :: IDB | e) Unit 122 | onError db' f = 123 | fromEffFnAff $ Fn.runFn2 _onError db' f 124 | 125 | 126 | -------------------- 127 | -- FFI 128 | -- 129 | 130 | foreign import _abort 131 | :: forall tx e 132 | . tx 133 | -> EffFnAff (idb :: IDB | e) Unit 134 | 135 | 136 | foreign import _db 137 | :: Transaction 138 | -> Database 139 | 140 | 141 | foreign import _error 142 | :: Transaction 143 | -> (Nullable Error) 144 | 145 | 146 | foreign import _mode 147 | :: Fn4 TransactionMode TransactionMode TransactionMode Transaction TransactionMode 148 | 149 | 150 | foreign import _objectStoreNames 151 | :: Transaction 152 | -> Array String 153 | 154 | 155 | foreign import _objectStore 156 | :: forall tx e 157 | . Fn2 tx String (EffFnAff (idb :: IDB | e) ObjectStore) 158 | 159 | 160 | foreign import _onAbort 161 | :: forall tx e e' 162 | . Fn2 tx (Eff ( | e') Unit) (EffFnAff (idb :: IDB | e) Unit) 163 | 164 | 165 | foreign import _onComplete 166 | :: forall tx e e' 167 | . Fn2 tx (Eff ( | e') Unit) (EffFnAff (idb :: IDB | e) Unit) 168 | 169 | 170 | foreign import _onError 171 | :: forall tx e e' 172 | . Fn2 tx (Error -> Eff ( | e') Unit) (EffFnAff (idb :: IDB | e) Unit) 173 | -------------------------------------------------------------------------------- /src/Database/IndexedDB/IDBCursor.js: -------------------------------------------------------------------------------- 1 | const errorHandler = function errorHandler(cb) { 2 | return function _handler(e) { 3 | cb(e.target.error); 4 | }; 5 | }; 6 | 7 | const successHandler = function successHandler(cb) { 8 | return function _handler(e) { 9 | cb(e.target.result); 10 | }; 11 | }; 12 | 13 | exports._advance = function _advance(cursor, count) { 14 | return function aff(error, success) { 15 | try { 16 | cursor.advance(count); 17 | success(); 18 | } catch (e) { 19 | error(e); 20 | } 21 | 22 | return function canceler(_, cancelerError) { 23 | cancelerError(new Error("Can't cancel IDB Effects")); 24 | }; 25 | }; 26 | }; 27 | 28 | exports._continue = function _continue(cursor, key) { 29 | return function aff(error, success) { 30 | try { 31 | cursor.continue(key || undefined); 32 | success(); 33 | } catch (e) { 34 | error(e); 35 | } 36 | 37 | return function canceler(_, cancelerError) { 38 | cancelerError(new Error("Can't cancel IDB Effects")); 39 | }; 40 | }; 41 | }; 42 | 43 | exports._continuePrimaryKey = function _continuePrimaryKey(cursor, key, primaryKey) { 44 | return function aff(error, success) { 45 | try { 46 | cursor.continuePrimaryKey(key, primaryKey); 47 | success(); 48 | } catch (e) { 49 | error(e); 50 | } 51 | 52 | return function canceler(_, cancelerError) { 53 | cancelerError(new Error("Can't cancel IDB Effects")); 54 | }; 55 | }; 56 | }; 57 | 58 | exports._delete = function _delete(cursor) { 59 | return function aff(error, success) { 60 | try { 61 | const request = cursor.delete(); 62 | request.onsuccess = successHandler(success); 63 | request.onerror = errorHandler(error); 64 | } catch (e) { 65 | error(e); 66 | } 67 | 68 | return function canceler(_, cancelerError) { 69 | cancelerError(new Error("Can't cancel IDB Effects")); 70 | }; 71 | }; 72 | }; 73 | 74 | exports._direction = function _direction(fromString, cursor) { 75 | return fromString(cursor.direction); 76 | }; 77 | 78 | exports._key = function _key(cursor) { 79 | return function aff(error, success) { 80 | try { 81 | success(cursor.key); 82 | } catch (e) { 83 | error(e); 84 | } 85 | 86 | return function canceler(_, cancelerError) { 87 | cancelerError(new Error("Can't cancel IDB Effects")); 88 | }; 89 | }; 90 | }; 91 | 92 | exports._primaryKey = function _primaryKey(cursor) { 93 | return function aff(error, success) { 94 | try { 95 | success(cursor.primaryKey); 96 | } catch (e) { 97 | error(e); 98 | } 99 | 100 | return function canceler(_, cancelerError) { 101 | cancelerError(new Error("Can't cancel IDB Effects")); 102 | }; 103 | }; 104 | }; 105 | 106 | exports._source = function _source(IDBObjectStore, IDBIndex, cursor) { 107 | switch (cursor.source.constructor.name) { 108 | case 'IDBIndex': 109 | return IDBIndex(cursor.source); 110 | case 'IDBObjectStore': 111 | return IDBObjectStore(cursor.source); 112 | default: 113 | throw Object.create(Error.prototype, { 114 | message: { 115 | enumerable: true, 116 | value: 'Unable to retrieve the cursor\'s source constructor.', 117 | }, 118 | name: { 119 | enumerable: true, 120 | value: 'UnexpectedCursorSource', 121 | }, 122 | }); 123 | } 124 | }; 125 | 126 | exports._update = function _update(cursor, value) { 127 | return function aff(error, success) { 128 | try { 129 | const request = cursor.update(value); 130 | request.onsuccess = successHandler(success); 131 | request.onerror = errorHandler(error); 132 | } catch (e) { 133 | error(e); 134 | } 135 | 136 | return function canceler(_, cancelerError) { 137 | cancelerError(new Error("Can't cancel IDB Effects")); 138 | }; 139 | }; 140 | }; 141 | 142 | exports._value = function _value(cursor) { 143 | return cursor.value; 144 | }; 145 | -------------------------------------------------------------------------------- /src/Database/IndexedDB/IDBKeyRange.purs: -------------------------------------------------------------------------------- 1 | -- | A key has an associated type which is one of: number, date, string, binary, or array. 2 | module Database.IndexedDB.IDBKeyRange 3 | -- * Types 4 | ( Open 5 | 6 | -- * Constructors 7 | , only 8 | , lowerBound 9 | , upperBound 10 | , bound 11 | 12 | -- * Interface 13 | , includes 14 | 15 | -- * Attributes 16 | , lower 17 | , upper 18 | , lowerOpen 19 | , upperOpen 20 | ) where 21 | 22 | import Prelude (($), (>>>), map) 23 | 24 | import Data.Foreign (Foreign) 25 | import Data.Function.Uncurried as Fn 26 | import Data.Function.Uncurried (Fn2, Fn4) 27 | import Data.Maybe (Maybe) 28 | import Data.Nullable (Nullable, toMaybe) 29 | 30 | import Database.IndexedDB.Core (class IDBKeyRange, KeyRange) 31 | import Database.IndexedDB.IDBKey.Internal (class IDBKey, Key, toKey, unsafeFromKey) 32 | 33 | 34 | -------------------- 35 | -- TYPES 36 | -- 37 | 38 | -- | Type alias for open 39 | type Open = Boolean 40 | 41 | 42 | -------------------- 43 | -- CONSTRUCTORS 44 | -- 45 | 46 | -- | Returns a new IDBKeyRange spanning only key. 47 | only 48 | :: forall a. (IDBKey a) 49 | => a 50 | -> KeyRange 51 | only key = 52 | _only (unsafeFromKey $ toKey key) 53 | 54 | 55 | -- | Returns a new IDBKeyRange starting at key with no upper bound. 56 | -- | If `Open` is `true`, key is not included in the range. 57 | lowerBound 58 | :: forall a. (IDBKey a) 59 | => a 60 | -> Open 61 | -> KeyRange 62 | lowerBound key open = 63 | Fn.runFn2 _lowerBound (unsafeFromKey $ toKey key) open 64 | 65 | 66 | -- | Returns a new IDBKeyRange with no lower bound and ending at key. 67 | -- | If `Open` is `true`, key is not included in the range. 68 | upperBound 69 | :: forall a. (IDBKey a) 70 | => a 71 | -> Open 72 | -> KeyRange 73 | upperBound key open = 74 | Fn.runFn2 _upperBound (unsafeFromKey $ toKey key) open 75 | 76 | 77 | -- | Returns a new IDBKeyRange spanning from `lower` to `upper`. 78 | -- | If `lowerOpen` is `true`, `lower` is not included in the range. 79 | -- | If `upperOpen` is `true`, `upper` is not included in the range. 80 | -- | 81 | -- | It throws a `DataError` if the bound is invalid. 82 | bound 83 | :: forall key. (IDBKey key) 84 | => { lower :: key, upper :: key, lowerOpen :: Boolean, upperOpen :: Boolean } 85 | -> Maybe KeyRange 86 | bound { lower: key1, upper: key2, lowerOpen: open1, upperOpen: open2 } = 87 | toMaybe 88 | $ Fn.runFn4 _bound (unsafeFromKey $ toKey key1) (unsafeFromKey $ toKey key2) open1 open2 89 | 90 | 91 | -------------------- 92 | -- INTERFACE 93 | -- 94 | 95 | -- | Returns true if key is included in the range, and false otherwise. 96 | includes 97 | :: forall key range. (IDBKey key) => (IDBKeyRange range) 98 | => range 99 | -> key 100 | -> Boolean 101 | includes range = 102 | toKey >>> unsafeFromKey >>> Fn.runFn2 _includes range 103 | 104 | 105 | -------------------- 106 | -- ATTRIBUTES 107 | -- 108 | -- | Returns lower bound if any. 109 | lower 110 | :: KeyRange 111 | -> Maybe Key 112 | lower = 113 | _lower >>> toMaybe >>> map toKey 114 | 115 | 116 | -- | Returns upper bound if any. 117 | upper 118 | :: KeyRange 119 | -> Maybe Key 120 | upper = 121 | _upper >>> toMaybe >>> map toKey 122 | 123 | 124 | -- | Returns true if the lower open flag is set, false otherwise. 125 | lowerOpen 126 | :: KeyRange 127 | -> Boolean 128 | lowerOpen = 129 | _lowerOpen 130 | 131 | 132 | -- | Returns true if the upper open flag is set, false otherwise. 133 | upperOpen 134 | :: KeyRange 135 | -> Boolean 136 | upperOpen = 137 | _upperOpen 138 | 139 | 140 | -------------------- 141 | -- FFI 142 | -- 143 | 144 | foreign import _only 145 | :: Foreign 146 | -> KeyRange 147 | 148 | foreign import _lowerBound 149 | :: Fn2 Foreign Boolean KeyRange 150 | 151 | 152 | foreign import _upperBound 153 | :: Fn2 Foreign Boolean KeyRange 154 | 155 | 156 | foreign import _bound 157 | :: Fn4 Foreign Foreign Boolean Boolean (Nullable KeyRange) 158 | 159 | 160 | foreign import _includes 161 | :: forall range 162 | . Fn2 range Foreign Boolean 163 | 164 | 165 | foreign import _lower 166 | :: KeyRange 167 | -> Nullable Foreign 168 | 169 | 170 | foreign import _upper 171 | :: KeyRange 172 | -> Nullable Foreign 173 | 174 | 175 | foreign import _lowerOpen 176 | :: KeyRange 177 | -> Boolean 178 | 179 | 180 | foreign import _upperOpen 181 | :: KeyRange 182 | -> Boolean 183 | -------------------------------------------------------------------------------- /src/Database/IndexedDB/IDBDatabase.js: -------------------------------------------------------------------------------- 1 | const toArray = function toArray(xs) { 2 | return Array.prototype.slice.apply(xs); 3 | }; 4 | 5 | 6 | exports._close = function _close(db) { 7 | return function aff(error, success) { 8 | try { 9 | db.close(); 10 | success(); 11 | } catch (e) { 12 | error(e); 13 | } 14 | 15 | return function canceler(_, cancelerError) { 16 | cancelerError(new Error("Can't cancel IDB Effects")); 17 | }; 18 | }; 19 | }; 20 | 21 | exports._createObjectStore = function _createObjectStore(db, name, opts) { 22 | return function aff(error, success) { 23 | var keyPath; 24 | 25 | try { 26 | // NOTE 1: createObjectStore throws when given an empty array 27 | // NOTE 2: keyPath supports strings and sequence of strings, however 28 | // a string hasn't the same meaning as a sequence of strings 29 | switch (opts.keyPath.length) { 30 | case 0: 31 | keyPath = undefined; 32 | break; 33 | case 1: 34 | keyPath = opts.keyPath[0]; 35 | break; 36 | default: 37 | keyPath = opts.keyPath; 38 | } 39 | 40 | const store = db.createObjectStore(name, { 41 | autoIncrement: opts.autoIncrement, 42 | keyPath: keyPath, 43 | }); 44 | success(store); 45 | } catch (e) { 46 | error(e); 47 | } 48 | 49 | return function canceler(_, cancelerError) { 50 | cancelerError(new Error("Can't cancel IDB Effects")); 51 | }; 52 | }; 53 | }; 54 | 55 | exports._deleteObjectStore = function _deleteObjectStore(db, name) { 56 | return function aff(error, success) { 57 | try { 58 | db.deleteObjectStore(name); 59 | success(); 60 | } catch (e) { 61 | error(e); 62 | } 63 | 64 | return function canceler(_, cancelerError) { 65 | cancelerError(new Error("Can't cancel IDB Effects")); 66 | }; 67 | }; 68 | }; 69 | 70 | exports._name = function _name(db) { 71 | return db.name; 72 | }; 73 | 74 | exports._objectStoreNames = function _objectStoreNames(db) { 75 | return toArray(db.objectStoreNames); 76 | }; 77 | 78 | exports._onAbort = function _onAbort(db, f) { 79 | return function aff(error, success) { 80 | db.onabort = function onabort() { 81 | f(); 82 | }; 83 | success(); 84 | 85 | return function canceler(_, cancelerError) { 86 | cancelerError(new Error("Can't cancel IDB Effects")); 87 | }; 88 | }; 89 | }; 90 | 91 | exports._onClose = function _onClose(db, f) { 92 | return function aff(error, success) { 93 | db.onclose = function onclose() { 94 | f(); 95 | }; 96 | success(); 97 | 98 | return function canceler(_, cancelerError) { 99 | cancelerError(new Error("Can't cancel IDB Effects")); 100 | }; 101 | }; 102 | }; 103 | 104 | exports._onError = function _onError(db, f) { 105 | return function aff(error, success) { 106 | db.onerror = function onerror(e) { 107 | f(e.target.error)(); 108 | }; 109 | success(); 110 | 111 | return function canceler(_, cancelerError) { 112 | cancelerError(new Error("Can't cancel IDB Effects")); 113 | }; 114 | }; 115 | }; 116 | 117 | exports._onVersionChange = function _onVersionChange(db, f) { 118 | return function aff(error, success) { 119 | db.onversionchange = function onversionchange(e) { 120 | f({ oldVersion: e.oldVersion, newVersion: e.newVersion })(); 121 | }; 122 | success(); 123 | 124 | return function canceler(_, cancelerError) { 125 | cancelerError(new Error("Can't cancel IDB Effects")); 126 | }; 127 | }; 128 | }; 129 | 130 | exports._transaction = function _transaction(db, stores, mode) { 131 | return function aff(error, success) { 132 | var transaction; 133 | try { 134 | transaction = db.transaction(stores, mode); 135 | success(transaction); 136 | } catch (e) { 137 | error(e); 138 | } 139 | 140 | return function canceler(_, cancelerError, cancelerSuccess) { 141 | transaction.abort(); 142 | cancelerSuccess(); 143 | }; 144 | }; 145 | }; 146 | 147 | exports._version = function _version(db) { 148 | return db.version; 149 | }; 150 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | PureScript IndexedDB [![](https://img.shields.io/badge/doc-pursuit-60b5cc.svg)](http://pursuit.purescript.org/packages/purescript-indexeddb) [![Build Status](https://travis-ci.org/truqu/purescript-indexedDB.svg?branch=master)](https://travis-ci.org/truqu/purescript-indexedDB) 2 | ===== 3 | 4 | This package offers complete bindings and type-safety upon the [IndexedDB API](https://w3c.github.io/IndexedDB). 5 | 6 | ## Overview 7 | 8 | The `IDBCore` and `IDBFactory` are the two entry points required to create and connect to an 9 | indexed database. From there, modules are divided such that each of them covers a specific IDB 10 | interface. 11 | 12 | They are designed to be used as qualified imports such that each method gets prefixed with a 13 | menaingful namespace (e.g `IDBIndex.get`, `IDBObjectStore.openCursor` ...) 14 | 15 | Here's a quick example of what it look likes. 16 | ```purescript 17 | module Main where 18 | 19 | import Prelude 20 | 21 | import Control.Monad.Aff (Aff, launchAff_) 22 | import Control.Monad.Aff.Console (CONSOLE, log) 23 | import Control.Monad.Eff.Exception (EXCEPTION) 24 | import Control.Monad.Eff (Eff) 25 | import Data.Maybe (Maybe(..), maybe) 26 | 27 | import Database.IndexedDB.Core 28 | import Database.IndexedDB.IDBFactory as IDBFactory 29 | import Database.IndexedDB.IDBDatabase as IDBDatabase 30 | import Database.IndexedDB.IDBObjectStore as IDBObjectStore 31 | import Database.IndexedDB.IDBIndex as IDBIndex 32 | import Database.IndexedDB.IDBTransaction as IDBTransaction 33 | import Database.IndexedDB.IDBKeyRange as IDBKeyRange 34 | 35 | 36 | main :: Eff (idb :: IDB, exception :: EXCEPTION, console :: CONSOLE) Unit 37 | main = launchAff_ do 38 | db <- IDBFactory.open "db" Nothing { onBlocked : Nothing 39 | , onUpgradeNeeded : Just onUpgradeNeeded 40 | } 41 | 42 | tx <- IDBDatabase.transaction db ["store"] ReadOnly 43 | store <- IDBTransaction.objectStore tx "store" 44 | (val :: Maybe String) <- IDBObjectStore.get store (IDBKeyRange.only 1) 45 | log $ maybe "not found" id val 46 | 47 | 48 | onUpgradeNeeded :: forall e. Database -> Transaction -> { oldVersion :: Int } -> Eff (idb :: IDB, exception :: EXCEPTION | e) Unit 49 | onUpgradeNeeded db _ _ = launchAff_ do 50 | store <- IDBDatabase.createObjectStore db "store" IDBDatabase.defaultParameters 51 | _ <- IDBObjectStore.add store "patate" (Just 1) 52 | _ <- IDBObjectStore.add store { property: 42 } (Just 2) 53 | _ <- IDBObjectStore.createIndex store "index" ["property"] IDBObjectStore.defaultParameters 54 | pure unit 55 | ``` 56 | 57 | ## Notes 58 | 59 | ### Errors 60 | Errors normally thrown by the IDB\* interfaces are wrapped in the `Aff` Monad as `Error` where 61 | the `name` corresponds to the error's name (e.g. "InvalidStateError"). Pattern matching can 62 | therefore be done on any error to handle specific errors thrown by the API. 63 | 64 | ### Examples 65 | The `test` folder contains a great amount of examples showing practical usage of the IDB\* 66 | interfaces. Do not hesitate to have a peek should you wonder how to use one of the module. The 67 | wrapper tries to keep as much as possible an API consistent with the original IndexedDB API. 68 | Hence, it should be quite straightforward to translate any JavaScript example to a PureScript 69 | one. 70 | 71 | ## Changelog 72 | 73 | #### v3.0.0 74 | 75 | - callback to `onUpgradeNeeded` event now provide a record with the database old version. 76 | 77 | #### v2.0.0 78 | 79 | - review interface implementation (use of opaque classes to improve readability without compromising 80 | the reusability). The API doesn't really change from a user perspective though. 81 | 82 | - make the Key more opaque (by having an IDBKey instance for Foreign types) 83 | 84 | - Upgrade purescript-exceptions to 3.1.0 and leverage the new `name` accessor 85 | 86 | 87 | #### v1.0.0 88 | 89 | - [Indexed Database API 2.0](https://w3c.github.io/IndexedDB/) totally covered apart from 90 | - `index.getAll` method (and the associated one for the IDBObjectStore) 91 | - binary keys 92 | 93 | ## Documentation 94 | 95 | Module documentation is [published on Pursuit](http://pursuit.purescript.org/packages/purescript-indexeddb). 96 | 97 | ## Testing 98 | Tested in the cloud on multiple browsers and operating systems thanks to [BrowserStack](https://www.browserstack.com) 99 | 100 | 101 | | IE / Edge | Chrome | Firefox | Safari | Opera | Android | iOS Safari | 102 | | ----------| ------ | ------- | ------- | ----- | ------- | ---------- | 103 | | - | >= 57 | >= 51 | - | >= 46 | - | - | 104 | 105 |

106 | browserstack 107 |

108 | -------------------------------------------------------------------------------- /src/Database/IndexedDB/IDBObjectStore.js: -------------------------------------------------------------------------------- 1 | const errorHandler = function errorHandler(cb) { 2 | return function _handler(e) { 3 | cb(e.target.error); 4 | }; 5 | }; 6 | 7 | const successHandler = function successHandler(cb) { 8 | return function _handler(e) { 9 | cb(e.target.result); 10 | }; 11 | }; 12 | 13 | const toArray = function toArray(xs) { 14 | return Array.prototype.slice.apply(xs); 15 | }; 16 | 17 | 18 | exports._add = function _add(store, value, key) { 19 | return function aff(error, success) { 20 | try { 21 | const request = store.add(value, key || undefined); 22 | request.onsuccess = successHandler(success); 23 | request.onerror = errorHandler(error); 24 | } catch (e) { 25 | error(e); 26 | } 27 | 28 | return function canceler(_, cancelerError) { 29 | cancelerError(new Error("Can't cancel IDB Effects")); 30 | }; 31 | }; 32 | }; 33 | 34 | exports._autoIncrement = function _autoIncrement(store) { 35 | return store.autoIncrement; 36 | }; 37 | 38 | exports._clear = function _clear(store) { 39 | return function aff(error, success) { 40 | try { 41 | const request = store.clear(); 42 | request.onsuccess = successHandler(success); 43 | request.onerror = errorHandler(error); 44 | } catch (e) { 45 | error(e); 46 | } 47 | 48 | return function canceler(_, cancelerError) { 49 | cancelerError(new Error("Can't cancel IDB Effects")); 50 | }; 51 | }; 52 | }; 53 | 54 | exports._createIndex = function _createIndex(store, name, path, params) { 55 | return function aff(error, success) { 56 | var keyPath; 57 | 58 | try { 59 | // NOTE: keyPath supports strings and sequence of strings, however 60 | // a string hasn't the same meaning as a sequence of strings 61 | switch (path.length) { 62 | case 0: 63 | keyPath = ''; 64 | break; 65 | case 1: 66 | keyPath = path[0]; 67 | break; 68 | default: 69 | keyPath = path; 70 | } 71 | 72 | const index = store.createIndex(name, keyPath, params); 73 | success(index); 74 | } catch (e) { 75 | error(e); 76 | } 77 | 78 | return function canceler(_, cancelerError) { 79 | cancelerError(new Error("Can't cancel IDB Effects")); 80 | }; 81 | }; 82 | }; 83 | 84 | exports._deleteIndex = function _deleteIndex(store, name) { 85 | return function aff(error, success) { 86 | try { 87 | store.deleteIndex(name); 88 | success(); 89 | } catch (e) { 90 | error(e); 91 | } 92 | 93 | return function canceler(_, cancelerError) { 94 | cancelerError(new Error("Can't cancel IDB Effects")); 95 | }; 96 | }; 97 | }; 98 | 99 | exports._delete = function _delete(store, query) { 100 | return function aff(error, success) { 101 | try { 102 | const request = store.delete(query); 103 | request.onsuccess = successHandler(success); 104 | request.onerror = errorHandler(error); 105 | } catch (e) { 106 | error(e); 107 | } 108 | 109 | return function canceler(_, cancelerError) { 110 | cancelerError(new Error("Can't cancel IDB Effects")); 111 | }; 112 | }; 113 | }; 114 | 115 | exports._index = function _index(store, name) { 116 | return function aff(error, success) { 117 | try { 118 | const index = store.index(name); 119 | success(index); 120 | } catch (e) { 121 | error(e); 122 | } 123 | 124 | return function canceler(_, cancelerError) { 125 | cancelerError(new Error("Can't cancel IDB Effects")); 126 | }; 127 | }; 128 | }; 129 | 130 | exports._indexNames = function _indexNames(store) { 131 | return toArray(store.indexNames); 132 | }; 133 | 134 | exports._keyPath = function _keyPath(store) { 135 | const path = store.keyPath; 136 | 137 | if (Array.isArray(path)) { 138 | return path; 139 | } 140 | 141 | if (typeof path === 'string') { 142 | return [path]; 143 | } 144 | 145 | return []; 146 | }; 147 | 148 | exports._name = function _name(store) { 149 | return store.name; 150 | }; 151 | 152 | exports._put = function _put(store, value, key) { 153 | return function aff(error, success) { 154 | try { 155 | const request = store.put(value, key || undefined); 156 | request.onsuccess = successHandler(success); 157 | request.onerror = errorHandler(error); 158 | } catch (e) { 159 | error(e); 160 | } 161 | 162 | return function canceler(_, cancelerError) { 163 | cancelerError(new Error("Can't cancel IDB Effects")); 164 | }; 165 | }; 166 | }; 167 | 168 | exports._transaction = function _transaction(store) { 169 | return store.transaction; 170 | }; 171 | -------------------------------------------------------------------------------- /src/Database/IndexedDB/IDBIndex.js: -------------------------------------------------------------------------------- 1 | const errorHandler = function errorHandler(cb) { 2 | return function _handler(e) { 3 | cb(e.target.error); 4 | }; 5 | }; 6 | 7 | const successHandler = function successHandler(cb) { 8 | return function _handler(e) { 9 | cb(e.target.result); 10 | }; 11 | }; 12 | 13 | 14 | exports._keyPath = function _keyPath(index) { 15 | const path = index.keyPath; 16 | 17 | if (Array.isArray(path)) { 18 | return path; 19 | } 20 | 21 | if (typeof path === 'string' && path !== '') { 22 | return [path]; 23 | } 24 | 25 | return []; 26 | }; 27 | 28 | exports._multiEntry = function _multiEntry(index) { 29 | return index.multiEntry; 30 | }; 31 | 32 | exports._name = function _name(index) { 33 | return index.name; 34 | }; 35 | 36 | exports._objectStore = function _objectStore(index) { 37 | return index.obectStore; 38 | }; 39 | 40 | exports._unique = function _unique(index) { 41 | return index.unique; 42 | }; 43 | 44 | exports._count = function _count(index, query) { 45 | return function aff(error, success) { 46 | try { 47 | const request = index.count(query); 48 | request.onsuccess = successHandler(success); 49 | request.onerror = errorHandler(error); 50 | } catch (e) { 51 | error(e); 52 | } 53 | 54 | return function canceler(_, cancelerError) { 55 | cancelerError(new Error("Can't cancel IDB Effects")); 56 | }; 57 | }; 58 | }; 59 | 60 | exports._get = function _get(index, range) { 61 | return function aff(error, success) { 62 | try { 63 | const request = index.get(range); 64 | request.onsuccess = successHandler(success); 65 | request.onerror = errorHandler(error); 66 | } catch (e) { 67 | error(e); 68 | } 69 | 70 | return function canceler(_, cancelerError) { 71 | cancelerError(new Error("Can't cancel IDB Effects")); 72 | }; 73 | }; 74 | }; 75 | 76 | /* 77 | * NOTE: Require some additional work. The array (which isn't necessarily a list of 78 | * polymorphic types in js) can't be easily translated to a PureScript array. 79 | * 80 | * However, it may be doable to convert the result to some key / value structure with values of 81 | * different types. 82 | exports._getAll = function _getAll(index, query, count) { 83 | return function aff(error, success) { 84 | const request = index.getAll(query, count); 85 | request.onsuccess = successHandler(success); 86 | request.onerror = errorHandler(error); 87 | }; 88 | }; 89 | */ 90 | 91 | exports._getAllKeys = function _getAllKeys(index, range, count) { 92 | return function aff(error, success) { 93 | try { 94 | const request = index.getAllKeys(range, count || undefined); 95 | request.onsuccess = successHandler(success); 96 | request.onerror = errorHandler(error); 97 | } catch (e) { 98 | error(e); 99 | } 100 | 101 | return function canceler(_, cancelerError) { 102 | cancelerError(new Error("Can't cancel IDB Effects")); 103 | }; 104 | }; 105 | }; 106 | 107 | exports._getKey = function _getKey(index, range) { 108 | return function aff(error, success) { 109 | try { 110 | const request = index.getKey(range); 111 | request.onsuccess = successHandler(success); 112 | request.onerror = errorHandler(error); 113 | } catch (e) { 114 | error(e); 115 | } 116 | 117 | return function canceler(_, cancelerError) { 118 | cancelerError(new Error("Can't cancel IDB Effects")); 119 | }; 120 | }; 121 | }; 122 | 123 | exports._openCursor = function _openCursor(index, query, dir, cb) { 124 | return function aff(error, success) { 125 | try { 126 | const request = index.openCursor(query, dir); 127 | request.onsuccess = function onSuccess(e) { 128 | if (e.target.result != null) { 129 | cb.onSuccess(e.target.result)(); 130 | } else { 131 | cb.onComplete(); 132 | } 133 | }; 134 | request.onerror = function onError(e) { 135 | cb.onError(e.target.error); 136 | }; 137 | success(); 138 | } catch (e) { 139 | error(e); 140 | } 141 | 142 | return function canceler(_, cancelerError) { 143 | cancelerError(new Error("Can't cancel IDB Effects")); 144 | }; 145 | }; 146 | }; 147 | 148 | exports._openKeyCursor = function _openKeyCursor(index, query, dir, cb) { 149 | return function aff(error, success) { 150 | try { 151 | const request = index.openKeyCursor(query, dir); 152 | request.onsuccess = function onSuccess(e) { 153 | if (e.target.result != null) { 154 | cb.onSuccess(e.target.result)(); 155 | } else { 156 | cb.onComplete(); 157 | } 158 | }; 159 | request.onerror = function onError(e) { 160 | cb.onError(e.target.error); 161 | }; 162 | success(); 163 | } catch (e) { 164 | error(e); 165 | } 166 | 167 | return function canceler(_, cancelerError) { 168 | cancelerError(new Error("Can't cancel IDB Effects")); 169 | }; 170 | }; 171 | }; 172 | -------------------------------------------------------------------------------- /src/Database/IndexedDB/IDBCursor.purs: -------------------------------------------------------------------------------- 1 | -- | A cursor is used to iterate over a range of records in an index or 2 | -- | an object store in a specific direction. 3 | module Database.IndexedDB.IDBCursor 4 | -- * Interface 5 | ( advance 6 | , continue 7 | , continuePrimaryKey 8 | , delete 9 | , update 10 | 11 | -- * Attributes 12 | , direction 13 | , key 14 | , primaryKey 15 | , source 16 | , value 17 | ) where 18 | 19 | import Prelude (Unit, ($), (>>>), (<<<), map) 20 | 21 | import Control.Monad.Aff (Aff) 22 | import Control.Monad.Aff.Compat (fromEffFnAff, EffFnAff) 23 | import Data.Foreign (Foreign, toForeign, unsafeFromForeign) 24 | import Data.Function.Uncurried as Fn 25 | import Data.Function.Uncurried (Fn2, Fn3) 26 | import Data.Maybe (Maybe) 27 | import Data.Nullable (Nullable, toNullable) 28 | import Data.String.Read (read) 29 | 30 | import Database.IndexedDB.Core 31 | import Database.IndexedDB.IDBKey.Internal (class IDBKey, Key, toKey, unsafeFromKey) 32 | 33 | 34 | -------------------- 35 | -- INTERFACES 36 | -- 37 | 38 | -- | Advances the cursor through the next count records in range. 39 | advance 40 | :: forall e cursor. (IDBCursor cursor) 41 | => cursor 42 | -> Int 43 | -> Aff (idb :: IDB | e) Unit 44 | advance c = 45 | fromEffFnAff <<< Fn.runFn2 _advance c 46 | 47 | 48 | -- | Advances the cursor to the next record in range matching or after key. 49 | continue 50 | :: forall e k cursor. (IDBKey k) => (IDBCursor cursor) 51 | => cursor 52 | -> Maybe k 53 | -> Aff (idb :: IDB | e) Unit 54 | continue c mk = 55 | fromEffFnAff $ Fn.runFn2 _continue c (toNullable $ map (toKey >>> unsafeFromKey) mk) 56 | 57 | 58 | -- | Advances the cursor to the next record in range matching or after key and primaryKey. Throws an "InvalidAccessError" DOMException if the source is not an index. 59 | continuePrimaryKey 60 | :: forall e k cursor. (IDBKey k) => (IDBCursor cursor) 61 | => cursor 62 | -> k 63 | -> k 64 | -> Aff (idb :: IDB | e) Unit 65 | continuePrimaryKey c k1 k2 = 66 | fromEffFnAff $ Fn.runFn3 _continuePrimaryKey c (unsafeFromKey $ toKey k1) (unsafeFromKey $ toKey k2) 67 | 68 | 69 | -- | Delete the record pointed at by the cursor with a new value. 70 | delete 71 | :: forall e cursor. (IDBCursor cursor) 72 | => cursor 73 | -> Aff (idb :: IDB | e) Unit 74 | delete = 75 | fromEffFnAff <<< _delete 76 | 77 | 78 | -- | Update the record pointed at by the cursor with a new value. 79 | -- | 80 | -- | Throws a "DataError" DOMException if the effective object store uses 81 | -- | in-line keys and the key would have changed. 82 | update 83 | :: forall val e cursor. (IDBCursor cursor) 84 | => cursor 85 | -> val 86 | -> Aff (idb :: IDB | e) Key 87 | update c = 88 | map toKey <<< fromEffFnAff <<< Fn.runFn2 _update c <<< toForeign 89 | 90 | 91 | -------------------- 92 | -- ATTRIBUTES 93 | -- 94 | 95 | -- | Returns the direction (Next|NextUnique|Prev|PrevUnique) of the cursor. 96 | direction 97 | :: forall cursor. (IDBConcreteCursor cursor) 98 | => cursor 99 | -> CursorDirection 100 | direction = 101 | Fn.runFn2 _direction (read >>> toNullable) 102 | 103 | 104 | -- | Returns the key of the cursor. Throws a "InvalidStateError" DOMException 105 | -- | if the cursor is advancing or is finished. 106 | key 107 | :: forall e cursor. (IDBConcreteCursor cursor) 108 | => cursor 109 | -> Aff (idb :: IDB | e) Key 110 | key = 111 | map toKey <<< fromEffFnAff <<< _key 112 | 113 | 114 | -- | Returns the effective key of the cursor. Throws a "InvalidStateError" DOMException 115 | -- | if the cursor is advancing or is finished. 116 | primaryKey 117 | :: forall e cursor. (IDBConcreteCursor cursor) 118 | => cursor 119 | -> Aff (idb :: IDB | e) Key 120 | primaryKey = 121 | map toKey <<< fromEffFnAff <<< _primaryKey 122 | 123 | 124 | -- | Returns the IDBObjectStore or IDBIndex the cursor was opened from. 125 | source 126 | :: forall cursor. (IDBConcreteCursor cursor) 127 | => cursor 128 | -> CursorSource 129 | source = 130 | Fn.runFn3 _source ObjectStore Index 131 | 132 | 133 | value 134 | :: forall val 135 | . ValueCursor 136 | -> val 137 | value = 138 | _value >>> unsafeFromForeign 139 | 140 | 141 | -------------------- 142 | -- FFI 143 | -- 144 | 145 | foreign import _advance 146 | :: forall cursor e 147 | . Fn2 cursor Int (EffFnAff (idb :: IDB | e) Unit) 148 | 149 | 150 | foreign import _continue 151 | :: forall cursor e 152 | . Fn2 cursor (Nullable Foreign) (EffFnAff (idb :: IDB | e) Unit) 153 | 154 | 155 | foreign import _continuePrimaryKey 156 | :: forall cursor e 157 | . Fn3 cursor Foreign Foreign (EffFnAff (idb :: IDB | e) Unit) 158 | 159 | 160 | foreign import _delete 161 | :: forall cursor e 162 | . cursor 163 | -> (EffFnAff (idb :: IDB | e) Unit) 164 | 165 | 166 | foreign import _direction 167 | :: forall cursor 168 | . Fn2 (String -> Nullable CursorDirection) cursor CursorDirection 169 | 170 | 171 | foreign import _key 172 | :: forall cursor e 173 | . cursor 174 | -> EffFnAff (idb :: IDB | e) Key 175 | 176 | 177 | foreign import _primaryKey 178 | :: forall cursor e 179 | . cursor 180 | -> EffFnAff (idb :: IDB | e) Key 181 | 182 | 183 | foreign import _source 184 | :: forall cursor 185 | . Fn3 (ObjectStore -> CursorSource) (Index -> CursorSource) cursor CursorSource 186 | 187 | 188 | foreign import _update 189 | :: forall cursor e 190 | . Fn2 cursor Foreign (EffFnAff (idb :: IDB | e) Foreign) 191 | 192 | 193 | foreign import _value 194 | :: forall cursor val 195 | . cursor 196 | -> val 197 | -------------------------------------------------------------------------------- /src/Database/IndexedDB/IDBDatabase.purs: -------------------------------------------------------------------------------- 1 | -- | Each origin has an associated set of databases. A database has zero or more object 2 | -- | stores which hold the data stored in the database. 3 | module Database.IndexedDB.IDBDatabase 4 | -- * Types 5 | ( StoreName 6 | , ObjectStoreParameters 7 | , defaultParameters 8 | 9 | -- * Interface 10 | , close 11 | , createObjectStore 12 | , deleteObjectStore 13 | , transaction 14 | 15 | -- * Attributes 16 | , name 17 | , objectStoreNames 18 | , version 19 | 20 | -- * Event handlers 21 | , onAbort 22 | , onClose 23 | , onError 24 | , onVersionChange 25 | ) where 26 | 27 | import Prelude (Unit, show, (<<<), ($)) 28 | 29 | import Control.Monad.Aff (Aff) 30 | import Control.Monad.Aff.Compat (fromEffFnAff, EffFnAff) 31 | import Control.Monad.Eff (Eff) 32 | import Control.Monad.Eff.Exception (Error) 33 | import Data.Function.Uncurried as Fn 34 | import Data.Function.Uncurried (Fn2, Fn3) 35 | 36 | import Database.IndexedDB.Core 37 | 38 | 39 | -------------------- 40 | -- TYPES 41 | -- 42 | 43 | -- | Type alias for StoreName 44 | type StoreName = String 45 | 46 | 47 | -- | Options provided when creating an object store. 48 | type ObjectStoreParameters = 49 | { keyPath :: KeyPath 50 | , autoIncrement :: Boolean 51 | } 52 | 53 | 54 | defaultParameters :: ObjectStoreParameters 55 | defaultParameters = 56 | { keyPath : [] 57 | , autoIncrement : false 58 | } 59 | 60 | 61 | -------------------- 62 | -- INTERFACE 63 | -- 64 | 65 | -- | Closes the connection once all running transactions have finished. 66 | close 67 | :: forall e db. (IDBDatabase db) 68 | => db 69 | -> Aff (idb :: IDB | e) Unit 70 | close = 71 | fromEffFnAff <<< _close 72 | 73 | 74 | -- | Creates a new object store with the given name and options and returns a new IDBObjectStore. 75 | -- | 76 | -- | Throws a "InvalidStateError" DOMException if not called within an upgrade transaction 77 | createObjectStore 78 | :: forall e db. (IDBDatabase db) 79 | => db 80 | -> StoreName 81 | -> ObjectStoreParameters 82 | -> Aff (idb :: IDB | e) ObjectStore 83 | createObjectStore db name' opts = 84 | fromEffFnAff $ Fn.runFn3 _createObjectStore db name' opts 85 | 86 | 87 | -- | Deletes the object store with the given name. 88 | -- | 89 | -- | Throws a "InvalidStateError" DOMException if not called within an upgrade transaction. 90 | deleteObjectStore 91 | :: forall e db. (IDBDatabase db) 92 | => db 93 | -> StoreName 94 | -> Aff (idb :: IDB | e) ObjectStore 95 | deleteObjectStore db name' = 96 | fromEffFnAff $ Fn.runFn2 _deleteObjectStore db name' 97 | 98 | 99 | -- | Returns a new transaction with the given mode (ReadOnly|ReadWrite) 100 | -- | and scope which in the form of an array of object store names. 101 | transaction 102 | :: forall e db. (IDBDatabase db) 103 | => db 104 | -> Array StoreName 105 | -> TransactionMode 106 | -> Aff (idb :: IDB | e) Transaction 107 | transaction db stores mode' = 108 | fromEffFnAff $ Fn.runFn3 _transaction db stores (show mode') 109 | 110 | 111 | -------------------- 112 | -- ATTRIBUTES 113 | -- 114 | 115 | -- | Returns the name of the database. 116 | name 117 | :: Database 118 | -> String 119 | name = 120 | _name 121 | 122 | 123 | -- | Returns a list of the names of object stores in the database. 124 | objectStoreNames 125 | :: Database 126 | -> Array String 127 | objectStoreNames = 128 | _objectStoreNames 129 | 130 | 131 | -- | Returns the version of the database. 132 | version 133 | :: Database 134 | -> Int 135 | version = 136 | _version 137 | 138 | 139 | -------------------- 140 | -- EVENT HANDLERS 141 | -- 142 | -- | Event handler for the `abort` event. 143 | onAbort 144 | :: forall e e' 145 | . Database 146 | -> Eff ( | e') Unit 147 | -> Aff (idb :: IDB | e) Unit 148 | onAbort db f = 149 | fromEffFnAff $ Fn.runFn2 _onAbort db f 150 | 151 | 152 | -- | Event handler for the `close` event. 153 | onClose 154 | :: forall e e' 155 | . Database 156 | -> Eff ( | e') Unit 157 | -> Aff (idb :: IDB | e) Unit 158 | onClose db f = 159 | fromEffFnAff $ Fn.runFn2 _onClose db f 160 | 161 | 162 | -- | Event handler for the `error` event. 163 | onError 164 | :: forall e e' 165 | . Database 166 | -> (Error -> Eff ( | e') Unit) 167 | -> Aff (idb :: IDB | e) Unit 168 | onError db f = 169 | fromEffFnAff $ Fn.runFn2 _onError db f 170 | 171 | 172 | -- | Event handler for the `versionchange` event. 173 | onVersionChange 174 | :: forall e e' 175 | . Database 176 | -> ({ oldVersion :: Int, newVersion :: Int } 177 | -> Eff ( | e') Unit) 178 | -> Aff (idb :: IDB | e) Unit 179 | onVersionChange db f = 180 | fromEffFnAff $ Fn.runFn2 _onVersionChange db f 181 | 182 | 183 | -------------------- 184 | -- FFI 185 | -- 186 | 187 | foreign import _close 188 | :: forall db e 189 | . db 190 | -> EffFnAff (idb :: IDB | e) Unit 191 | 192 | 193 | foreign import _createObjectStore 194 | :: forall db e 195 | . Fn3 db String { keyPath :: Array String, autoIncrement :: Boolean } (EffFnAff (idb :: IDB | e) ObjectStore) 196 | 197 | 198 | foreign import _deleteObjectStore 199 | :: forall db e 200 | . Fn2 db String (EffFnAff (idb :: IDB | e) ObjectStore) 201 | 202 | 203 | foreign import _name 204 | :: Database 205 | -> String 206 | 207 | 208 | foreign import _objectStoreNames 209 | :: Database 210 | -> Array String 211 | 212 | 213 | foreign import _onAbort 214 | :: forall db e e' 215 | . Fn2 db (Eff ( | e') Unit) (EffFnAff (idb :: IDB | e) Unit) 216 | 217 | 218 | foreign import _onClose 219 | :: forall db e e' 220 | . Fn2 db (Eff ( | e') Unit) (EffFnAff (idb :: IDB | e) Unit) 221 | 222 | 223 | foreign import _onError 224 | :: forall db e e' 225 | . Fn2 db (Error -> Eff ( | e') Unit) (EffFnAff (idb :: IDB | e) Unit) 226 | 227 | 228 | foreign import _onVersionChange 229 | :: forall db e e' 230 | . Fn2 db ({ oldVersion :: Int, newVersion :: Int } -> Eff ( | e') Unit) (EffFnAff (idb :: IDB | e) Unit) 231 | 232 | 233 | foreign import _transaction 234 | :: forall db e 235 | . Fn3 db (Array String) String (EffFnAff (idb :: IDB | e) Transaction) 236 | 237 | 238 | foreign import _version 239 | :: Database 240 | -> Int 241 | -------------------------------------------------------------------------------- /src/Database/IndexedDB/IDBIndex.purs: -------------------------------------------------------------------------------- 1 | -- | An index allows looking up records in an object store using properties of the values 2 | -- | in the object stores records. 3 | module Database.IndexedDB.IDBIndex 4 | -- * Types 5 | ( Callbacks 6 | 7 | -- * Interface 8 | , count 9 | , get 10 | , getAllKeys 11 | , getKey 12 | , openCursor 13 | , openKeyCursor 14 | 15 | -- * Attributes 16 | , keyPath 17 | , multiEntry 18 | , name 19 | , objectStore 20 | , unique 21 | ) where 22 | 23 | import Prelude (Unit, map, show, (<$>), (>>>), ($)) 24 | 25 | import Control.Monad.Aff (Aff) 26 | import Control.Monad.Aff.Compat (EffFnAff, fromEffFnAff) 27 | import Control.Monad.Eff (Eff) 28 | import Control.Monad.Eff.Exception (Error) 29 | import Data.Foreign (Foreign, unsafeFromForeign) 30 | import Data.Function.Uncurried as Fn 31 | import Data.Function.Uncurried (Fn2, Fn3, Fn4) 32 | import Data.Maybe (Maybe) 33 | import Data.Nullable (Nullable, toMaybe, toNullable) 34 | 35 | import Database.IndexedDB.Core 36 | import Database.IndexedDB.IDBKey.Internal (Key, toKey) 37 | 38 | 39 | -------------------- 40 | -- TYPES 41 | -- 42 | 43 | -- | Callbacks to manipulate a cursor from an Open*Cursor call 44 | type Callbacks cursor e = 45 | { onSuccess :: cursor -> Eff ( | e) Unit 46 | , onError :: Error -> Eff ( | e) Unit 47 | , onComplete :: Eff ( | e) Unit 48 | } 49 | 50 | 51 | -------------------- 52 | -- INTERFACES 53 | -- 54 | 55 | -- | Retrieves the number of records matching the key range in query. 56 | count 57 | :: forall e index. (IDBIndex index) 58 | => index 59 | -> Maybe KeyRange 60 | -> Aff (idb :: IDB | e) Int 61 | count index range = 62 | fromEffFnAff $ Fn.runFn2 _count index (toNullable range) 63 | 64 | 65 | -- | Retrieves the value of the first record matching the given key range in query. 66 | -- | 67 | -- | NOTE 68 | -- | The coercion from `a` to any type is unsafe and might throw a runtime error if incorrect. 69 | get 70 | :: forall a e index. (IDBIndex index) 71 | => index 72 | -> KeyRange 73 | -> Aff (idb :: IDB | e) (Maybe a) 74 | get index range = 75 | map (toMaybe >>> map unsafeFromForeign) $ fromEffFnAff $ Fn.runFn2 _get index range 76 | 77 | 78 | -- | Retrieves the keys of records matching the given key range in query 79 | -- | (up to the number given if given). 80 | getAllKeys 81 | :: forall e index. (IDBIndex index) 82 | => index 83 | -> Maybe KeyRange 84 | -> Maybe Int 85 | -> Aff (idb :: IDB | e) (Array Key) 86 | getAllKeys index range n = 87 | map (map toKey) $ fromEffFnAff $ Fn.runFn3 _getAllKeys index (toNullable range) (toNullable n) 88 | 89 | 90 | -- | Retrieves the key of the first record matching the given key or key range in query. 91 | getKey 92 | :: forall e index. (IDBIndex index) 93 | => index 94 | -> KeyRange 95 | -> Aff (idb :: IDB | e) (Maybe Key) 96 | getKey index range = 97 | map (toMaybe >>> map toKey) $ fromEffFnAff $ Fn.runFn2 _getKey index range 98 | 99 | 100 | -- | Opens a ValueCursor over the records matching query, ordered by direction. 101 | -- | If query is `Nothing`, all records in index are matched. 102 | openCursor 103 | :: forall e e' index. (IDBIndex index) 104 | => index 105 | -> Maybe KeyRange 106 | -> CursorDirection 107 | -> Callbacks ValueCursor e' 108 | -> Aff (idb :: IDB | e) Unit 109 | openCursor index range dir cb = 110 | fromEffFnAff $ Fn.runFn4 _openCursor index (toNullable range) (show dir) cb 111 | 112 | 113 | -- | Opens a KeyCursor over the records matching query, ordered by direction. 114 | -- | If query is `Nothing`, all records in index are matched. 115 | openKeyCursor 116 | :: forall e e' index. (IDBIndex index) 117 | => index 118 | -> Maybe KeyRange 119 | -> CursorDirection 120 | -> Callbacks KeyCursor e' 121 | -> Aff (idb :: IDB | e) Unit 122 | openKeyCursor index range dir cb = 123 | fromEffFnAff $ Fn.runFn4 _openKeyCursor index (toNullable range) (show dir) cb 124 | 125 | 126 | -------------------- 127 | -- ATTRIBUTES 128 | -- 129 | -- | Returns the key path of the index. 130 | keyPath 131 | :: Index 132 | -> KeyPath 133 | keyPath = 134 | _keyPath 135 | 136 | 137 | -- | Returns true if the index's multiEntry flag is set. 138 | multiEntry 139 | :: Index 140 | -> Boolean 141 | multiEntry = 142 | _multiEntry 143 | 144 | 145 | -- | Returns the name of the index. 146 | name 147 | :: Index 148 | -> String 149 | name = 150 | _name 151 | 152 | 153 | -- | Returns the IDBObjectStore the index belongs to. 154 | objectStore 155 | :: Index 156 | -> ObjectStore 157 | objectStore = 158 | _objectStore 159 | 160 | 161 | -- | Returns true if the index's unique flag is set. 162 | unique 163 | :: Index 164 | -> Boolean 165 | unique = 166 | _unique 167 | 168 | 169 | -------------------- 170 | -- FFI 171 | -- 172 | foreign import _keyPath 173 | :: Index 174 | -> Array String 175 | 176 | 177 | foreign import _multiEntry 178 | :: Index 179 | -> Boolean 180 | 181 | 182 | foreign import _name 183 | :: Index 184 | -> String 185 | 186 | 187 | foreign import _objectStore 188 | :: Index 189 | -> ObjectStore 190 | 191 | 192 | foreign import _unique 193 | :: Index 194 | -> Boolean 195 | 196 | 197 | foreign import _count 198 | :: forall index e 199 | . Fn2 index (Nullable KeyRange) (EffFnAff (idb :: IDB | e) Int) 200 | 201 | 202 | foreign import _get 203 | :: forall index e 204 | . Fn2 index KeyRange (EffFnAff (idb :: IDB | e) (Nullable Foreign)) 205 | 206 | 207 | foreign import _getAllKeys 208 | :: forall index e 209 | . Fn3 index (Nullable KeyRange) (Nullable Int) (EffFnAff (idb :: IDB | e) (Array Foreign)) 210 | 211 | 212 | foreign import _getKey 213 | :: forall index e 214 | . Fn2 index KeyRange (EffFnAff (idb :: IDB | e) (Nullable Foreign)) 215 | 216 | 217 | foreign import _openCursor 218 | :: forall index e e' 219 | . Fn4 index (Nullable KeyRange) String (Callbacks ValueCursor e') (EffFnAff (idb :: IDB | e) Unit) 220 | 221 | 222 | foreign import _openKeyCursor 223 | :: forall index e e' 224 | . Fn4 index (Nullable KeyRange) String (Callbacks KeyCursor e') (EffFnAff (idb :: IDB | e) Unit) 225 | -------------------------------------------------------------------------------- /src/Database/IndexedDB/IDBKey/Internal.purs: -------------------------------------------------------------------------------- 1 | -- | A key has an associated type which is one of: number, date, string, or array. 2 | -- | 3 | -- | NOTE: Binary keys aren't supported yet. 4 | module Database.IndexedDB.IDBKey.Internal 5 | ( Key 6 | , class IDBKey, toKey , fromKey , unsafeFromKey 7 | , none 8 | ) where 9 | 10 | import Prelude 11 | 12 | import Control.Alt ((<|>)) 13 | import Control.Monad.Except (ExceptT(..), runExceptT) 14 | import Data.Date as Date 15 | import Data.DateTime (DateTime(..), Time(..)) 16 | import Data.Either (Either(..), either, isRight) 17 | import Data.Enum (fromEnum, toEnum) 18 | import Data.Foreign as Foreign 19 | import Data.Foreign (Foreign, F) 20 | import Data.Function.Uncurried as Fn 21 | import Data.Function.Uncurried (Fn2, Fn4, Fn7) 22 | import Data.Identity (Identity(..)) 23 | import Data.List.NonEmpty (NonEmptyList(..)) 24 | import Data.List.Types (List(..)) 25 | import Data.Maybe (Maybe(..)) 26 | import Data.NonEmpty (NonEmpty(..)) 27 | import Data.Nullable (Nullable, toNullable) 28 | import Data.Time as Time 29 | import Data.Traversable (traverse) 30 | 31 | 32 | newtype Key = Key Foreign 33 | 34 | 35 | -------------------- 36 | -- INTERFACES 37 | -- 38 | -- | Interface describing a key. Use the `unsafeFromKey` to convert a key 39 | -- | to a known type (e.g if you only strings as keys, or perfectly knows the 40 | -- | type of a given key). 41 | class IDBKey a where 42 | toKey :: a -> Key 43 | fromKey :: Key -> F a 44 | unsafeFromKey :: Key -> a 45 | 46 | 47 | none :: Maybe Key 48 | none = 49 | Nothing 50 | 51 | 52 | -------------------- 53 | -- INSTANCES 54 | -- 55 | instance eqKey :: Eq Key where 56 | eq a b = (runExceptT >>> runIdentity >>> isRight) $ 57 | eq <$> ((fromKey a) :: F Int) <*> fromKey b 58 | <|> 59 | eq <$> ((fromKey a) :: F Number) <*> fromKey b 60 | <|> 61 | eq <$> ((fromKey a) :: F String) <*> fromKey b 62 | <|> 63 | eq <$> ((fromKey a) :: F DateTime) <*> fromKey b 64 | <|> 65 | eq <$> ((fromKey a) :: F (Array Key)) <*> fromKey b 66 | where 67 | runIdentity :: forall a. Identity a -> a 68 | runIdentity (Identity x) = x 69 | 70 | 71 | instance ordKey :: Ord Key where 72 | compare a b = (runExceptT >>> runIdentity >>> either (const LT) id) $ 73 | compare <$> ((fromKey a) :: F Int) <*> fromKey b 74 | <|> 75 | compare <$> ((fromKey a) :: F Number) <*> fromKey b 76 | <|> 77 | compare <$> ((fromKey a) :: F String) <*> fromKey b 78 | <|> 79 | compare <$> ((fromKey a) :: F DateTime) <*> fromKey b 80 | <|> 81 | compare <$> ((fromKey a) :: F (Array Key)) <*> fromKey b 82 | where 83 | runIdentity :: forall a. Identity a -> a 84 | runIdentity (Identity x) = x 85 | 86 | 87 | instance showKey :: Show Key where 88 | show a = (runExceptT >>> format) $ 89 | (show <$> (fromKey a :: F Int)) 90 | <|> 91 | (show <$> (fromKey a :: F Number)) 92 | <|> 93 | (show <$> (fromKey a :: F String)) 94 | <|> 95 | (show <$> (fromKey a :: F DateTime)) 96 | <|> 97 | (show <$> (fromKey a :: F (Array Key))) 98 | where 99 | format :: forall a. Identity (Either a String) -> String 100 | format (Identity x) = 101 | either (const "(Key)") (\s -> "(Key " <> s <> ")") x 102 | 103 | 104 | instance idbKeyKey :: IDBKey Key where 105 | toKey = id 106 | fromKey = pure 107 | unsafeFromKey = id 108 | 109 | 110 | instance idbKeyForeign :: IDBKey Foreign where 111 | toKey = Key 112 | fromKey (Key f) = pure f 113 | unsafeFromKey (Key f) = f 114 | 115 | 116 | instance idbKeyInt :: IDBKey Int where 117 | toKey = Foreign.toForeign >>> Key 118 | fromKey (Key f) = Foreign.readInt f 119 | unsafeFromKey (Key f) = Foreign.unsafeFromForeign f 120 | 121 | 122 | instance idbKeyNumber :: IDBKey Number where 123 | toKey = Foreign.toForeign >>> Key 124 | fromKey (Key f) = Foreign.readNumber f 125 | unsafeFromKey (Key f) = Foreign.unsafeFromForeign f 126 | 127 | 128 | instance idbKeyString :: IDBKey String where 129 | toKey = Foreign.toForeign >>> Key 130 | fromKey (Key f) = Foreign.readString f 131 | unsafeFromKey (Key f) = Foreign.unsafeFromForeign f 132 | 133 | 134 | instance idbKeyDate :: IDBKey DateTime where 135 | toKey (DateTime d t) = Key $ Fn.runFn7 _dateTimeToForeign 136 | (fromEnum $ Date.year d) 137 | (fromEnum $ Date.month d) 138 | (fromEnum $ Date.day d) 139 | (fromEnum $ Time.hour t) 140 | (fromEnum $ Time.minute t) 141 | (fromEnum $ Time.second t) 142 | (fromEnum $ Time.millisecond t) 143 | fromKey (Key f) = Fn.runFn4 _readDateTime dateTime dateTimeF dateTimeE f 144 | unsafeFromKey (Key f) = Fn.runFn2 _unsafeReadDateTime dateTime f 145 | 146 | 147 | instance idbKeyArray :: IDBKey a => IDBKey (Array a) where 148 | toKey = Foreign.toForeign >>> Key 149 | fromKey (Key f) = Foreign.readArray f >>= traverse (Key >>> fromKey) 150 | unsafeFromKey (Key f) = map unsafeFromKey (Foreign.unsafeFromForeign f) 151 | 152 | 153 | -- FFI constructor to build a DateTime from years, months, days, hours, minutes, seconds and millis 154 | dateTime 155 | :: Int -- ^ years 156 | -> Int -- ^ months 157 | -> Int -- ^ days 158 | -> Int -- ^ hours 159 | -> Int -- ^ minutes 160 | -> Int -- ^ seconds 161 | -> Int -- ^ milliseconds 162 | -> Nullable DateTime 163 | dateTime y m d h mi s ms = 164 | toNullable $ DateTime 165 | <$> (Date.canonicalDate <$> toEnum y <*> toEnum m <*> toEnum d) 166 | <*> (Time <$> toEnum h <*> toEnum mi <*> toEnum s <*> toEnum ms) 167 | 168 | 169 | -- FFI constructor to convert a JS `Date` into a successful `F DateTime` 170 | dateTimeF 171 | :: DateTime 172 | -> F DateTime 173 | dateTimeF = 174 | Right >>> Identity >>> ExceptT 175 | 176 | 177 | -- FFI constructor to convert a string into an errored `F DateTime` 178 | dateTimeE 179 | :: String 180 | -> F DateTime 181 | dateTimeE = 182 | Foreign.TypeMismatch "Date" >>> flip NonEmpty Nil >>> NonEmptyList >>> Left >>> Identity >>> ExceptT 183 | 184 | 185 | -------------------- 186 | -- FFI 187 | -- 188 | foreign import _dateTimeToForeign 189 | :: Fn7 Int Int Int Int Int Int Int Foreign 190 | 191 | 192 | foreign import _readDateTime 193 | :: Fn4 (Int -> Int -> Int -> Int -> Int -> Int -> Int -> Nullable DateTime) (DateTime -> F DateTime) (String -> F DateTime) Foreign (F DateTime) 194 | 195 | 196 | foreign import _unsafeReadDateTime 197 | :: Fn2 (Int -> Int -> Int -> Int -> Int -> Int -> Int -> Nullable DateTime) Foreign DateTime 198 | -------------------------------------------------------------------------------- /src/Database/IndexedDB/IDBObjectStore.purs: -------------------------------------------------------------------------------- 1 | -- | An object store is the primary storage mechanism for storing data in a database. 2 | module Database.IndexedDB.IDBObjectStore 3 | -- * Types 4 | ( IndexName 5 | , IndexParameters 6 | , defaultParameters 7 | 8 | -- * Interface 9 | , add 10 | , clear 11 | , createIndex 12 | , delete 13 | , deleteIndex 14 | , index 15 | , put 16 | 17 | -- * Attributes 18 | , autoIncrement 19 | , indexNames 20 | , keyPath 21 | , name 22 | , transaction 23 | 24 | -- * Re-Exports 25 | , module Database.IndexedDB.IDBIndex 26 | ) where 27 | 28 | import Prelude (Unit, map, ($), (<$>), (>>>), (<<<)) 29 | 30 | import Control.Monad.Aff (Aff) 31 | import Control.Monad.Aff.Compat (EffFnAff, fromEffFnAff) 32 | import Data.Foreign (Foreign) 33 | import Data.Function.Uncurried as Fn 34 | import Data.Function.Uncurried (Fn2, Fn3, Fn4) 35 | import Data.Maybe (Maybe) 36 | import Data.Nullable (Nullable, toNullable) 37 | 38 | import Database.IndexedDB.Core 39 | import Database.IndexedDB.IDBIndex (count, get, getAllKeys, getKey, openCursor, openKeyCursor) 40 | import Database.IndexedDB.IDBKey.Internal (class IDBKey, Key, unsafeFromKey, toKey) 41 | 42 | -------------------- 43 | -- TYPES 44 | -- 45 | 46 | -- | Type alias for IndexName 47 | type IndexName = String 48 | 49 | 50 | -- | Flags to set on the index. 51 | -- | 52 | -- | An index has a `unique` flag. When this flag is set, the index enforces that no 53 | -- | two records in the index has the same key. If a record in the index’s referenced 54 | -- | object store is attempted to be inserted or modified such that evaluating the index’s 55 | -- | key path on the records new value yields a result which already exists in the index, 56 | -- | then the attempted modification to the object store fails. 57 | -- | 58 | -- | An index has a `multiEntry` flag. This flag affects how the index behaves when the 59 | -- | result of evaluating the index’s key path yields an array key. If the `multiEntry` flag 60 | -- | is unset, then a single record whose key is an array key is added to the index. 61 | -- | If the `multiEntry` flag is true, then the one record is added to the index for each 62 | -- | of the subkeys. 63 | type IndexParameters = 64 | { unique :: Boolean 65 | , multiEntry :: Boolean 66 | } 67 | 68 | 69 | defaultParameters :: IndexParameters 70 | defaultParameters = 71 | { unique : false 72 | , multiEntry : false 73 | } 74 | 75 | 76 | -------------------- 77 | -- INTERFACES 78 | -- 79 | 80 | -- | Adds or updates a record in store with the given value and key. 81 | -- | 82 | -- | If the store uses in-line keys and key is specified a "DataError" DOMException 83 | -- | will be thrown. 84 | -- | 85 | -- | If add() is used, and if a record with the key already exists the request will fail, 86 | -- | with a "ConstraintError" DOMException. 87 | add 88 | :: forall e key val store. (IDBKey key) => (IDBObjectStore store) 89 | => store 90 | -> val 91 | -> Maybe key 92 | -> Aff (idb :: IDB | e) Key 93 | add store value key = 94 | map toKey $ fromEffFnAff $ Fn.runFn3 _add store value (toNullable $ (toKey >>> unsafeFromKey) <$> key) 95 | 96 | 97 | -- | Deletes all records in store. 98 | clear 99 | :: forall e store. (IDBObjectStore store) 100 | => store 101 | -> Aff (idb :: IDB | e) Unit 102 | clear = 103 | fromEffFnAff <<< _clear 104 | 105 | 106 | -- | Creates a new index in store with the given name, keyPath and options and 107 | -- | returns a new IDBIndex. If the keyPath and options define constraints that 108 | -- | cannot be satisfied with the data already in store the upgrade transaction 109 | -- | will abort with a "ConstraintError" DOMException. 110 | -- | 111 | -- | Throws an "InvalidStateError" DOMException if not called within an upgrade transaction. 112 | createIndex 113 | :: forall e store. (IDBObjectStore store) 114 | => store 115 | -> IndexName 116 | -> KeyPath 117 | -> IndexParameters 118 | -> Aff (idb :: IDB | e) Index 119 | createIndex store name' path params = 120 | fromEffFnAff $ Fn.runFn4 _createIndex store name' path params 121 | 122 | 123 | -- | Deletes records in store with the given key or in the given key range in query. 124 | delete 125 | :: forall e store. (IDBObjectStore store) 126 | => store 127 | -> KeyRange 128 | -> Aff (idb :: IDB | e) Unit 129 | delete store range = 130 | fromEffFnAff $ Fn.runFn2 _delete store range 131 | 132 | 133 | -- | Deletes the index in store with the given name. 134 | -- | 135 | -- | Throws an "InvalidStateError" DOMException if not called within an upgrade transaction. 136 | deleteIndex 137 | :: forall e store. (IDBObjectStore store) 138 | => store 139 | -> IndexName 140 | -> Aff (idb :: IDB | e) Unit 141 | deleteIndex store name' = 142 | fromEffFnAff $ Fn.runFn2 _deleteIndex store name' 143 | 144 | 145 | -- | Returns an IDBIndex for the index named name in store. 146 | index 147 | :: forall e store. (IDBObjectStore store) 148 | => store 149 | -> IndexName 150 | -> Aff (idb :: IDB | e) Index 151 | index store name' = 152 | fromEffFnAff $ Fn.runFn2 _index store name' 153 | 154 | 155 | -- | Adds or updates a record in store with the given value and key. 156 | -- | 157 | -- | If the store uses in-line keys and key is specified a "DataError" DOMException 158 | -- | will be thrown. 159 | -- | 160 | -- | If put() is used, any existing record with the key will be replaced. 161 | put 162 | :: forall e val key store. (IDBKey key) => (IDBObjectStore store) 163 | => store 164 | -> val 165 | -> Maybe key 166 | -> Aff (idb :: IDB | e) Key 167 | put store value key = 168 | map toKey $ fromEffFnAff $ Fn.runFn3 _put store value (toNullable $ (toKey >>> unsafeFromKey) <$> key) 169 | 170 | 171 | -------------------- 172 | -- ATTRIBUTES 173 | -- 174 | -- | Returns `true` if the store has a key generator, and `false` otherwise. 175 | autoIncrement 176 | :: ObjectStore 177 | -> Boolean 178 | autoIncrement = 179 | _autoIncrement 180 | 181 | 182 | --| Returns a list of the names of indexes in the store. 183 | indexNames 184 | :: ObjectStore 185 | -> Array String 186 | indexNames = 187 | _indexNames 188 | 189 | 190 | -- | Returns the key path of the store, or empty array if none 191 | keyPath 192 | :: ObjectStore 193 | -> Array String 194 | keyPath = 195 | _keyPath 196 | 197 | 198 | -- | Returns the name of the store. 199 | name 200 | :: ObjectStore 201 | -> String 202 | name = 203 | _name 204 | 205 | 206 | -- | Returns the associated transaction. 207 | transaction 208 | :: ObjectStore 209 | -> Transaction 210 | transaction = 211 | _transaction 212 | 213 | 214 | -------------------- 215 | -- FFI 216 | -- 217 | foreign import _add 218 | :: forall e val store 219 | . Fn3 store val (Nullable Foreign) (EffFnAff (idb :: IDB | e) Foreign) 220 | 221 | 222 | foreign import _autoIncrement 223 | :: ObjectStore 224 | -> Boolean 225 | 226 | 227 | foreign import _clear 228 | :: forall e store 229 | . store 230 | -> EffFnAff (idb :: IDB | e) Unit 231 | 232 | 233 | foreign import _createIndex 234 | :: forall e store 235 | . Fn4 store String (Array String) { unique :: Boolean, multiEntry :: Boolean } (EffFnAff (idb :: IDB | e) Index) 236 | 237 | 238 | foreign import _delete 239 | :: forall e store 240 | . Fn2 store KeyRange (EffFnAff (idb :: IDB | e) Unit) 241 | 242 | 243 | foreign import _deleteIndex 244 | :: forall e store 245 | . Fn2 store String (EffFnAff (idb :: IDB | e) Unit) 246 | 247 | 248 | foreign import _index 249 | :: forall e store 250 | . Fn2 store String (EffFnAff (idb :: IDB | e) Index) 251 | 252 | 253 | foreign import _indexNames 254 | :: ObjectStore 255 | -> Array String 256 | 257 | 258 | foreign import _keyPath 259 | :: ObjectStore 260 | -> Array String 261 | 262 | 263 | foreign import _name 264 | :: ObjectStore 265 | -> String 266 | 267 | 268 | foreign import _put 269 | :: forall e val store 270 | . Fn3 store val (Nullable Foreign) (EffFnAff (idb :: IDB | e) Foreign) 271 | 272 | 273 | foreign import _transaction 274 | :: ObjectStore 275 | -> Transaction 276 | -------------------------------------------------------------------------------- /src/Database/IndexedDB/Core.purs: -------------------------------------------------------------------------------- 1 | -- | The Core module gathers types used across the library and provides basic Show instances for 2 | -- | those types. 3 | module Database.IndexedDB.Core 4 | -- * Effects 5 | ( IDB 6 | 7 | -- * Types 8 | , Database 9 | , Index 10 | , KeyCursor 11 | , KeyRange 12 | , ObjectStore 13 | , Transaction 14 | , ValueCursor 15 | , CursorDirection(..) 16 | , CursorSource(..) 17 | , KeyPath 18 | , TransactionMode(..) 19 | 20 | -- * Classes 21 | , class IDBConcreteCursor 22 | , class IDBCursor 23 | , class IDBDatabase 24 | , class IDBIndex 25 | , class IDBKeyRange 26 | , class IDBTransaction 27 | , class IDBObjectStore 28 | 29 | -- * Re-Exports 30 | , module Database.IndexedDB.IDBKey 31 | ) where 32 | 33 | import Prelude (class Show) 34 | 35 | import Control.Monad.Eff (kind Effect) 36 | import Data.Maybe (Maybe(..)) 37 | import Data.String.Read (class Read) 38 | 39 | import Database.IndexedDB.IDBKey 40 | 41 | 42 | -------------------- 43 | -- Effects 44 | -- 45 | 46 | -- | IDB Effects, manifestation that something happened with the IndexedDB 47 | foreign import data IDB :: Effect 48 | 49 | 50 | -------------------- 51 | -- TYPES 52 | -- 53 | 54 | -- | Each origin has an associated set of databases. A database has zero or more object 55 | -- | stores which hold the data stored in the database. 56 | foreign import data Database :: Type 57 | 58 | 59 | -- | An index allows looking up records in an object store using properties of the values 60 | -- | in the object stores records. 61 | foreign import data Index :: Type 62 | 63 | 64 | -- | A key range is a continuous interval over some data type used for keys. 65 | foreign import data KeyRange :: Type 66 | 67 | 68 | -- | A cursor is used to iterate over a range of records in an index or an object store 69 | -- | in a specific direction. A KeyCursor doesn't hold any value. 70 | foreign import data KeyCursor :: Type 71 | 72 | 73 | -- | A Transaction is used to interact with the data in a database. 74 | -- | Whenever data is read or written to the database it is done by using a transaction. 75 | foreign import data Transaction :: Type 76 | 77 | 78 | -- | An object store is the primary storage mechanism for storing data in a database. 79 | foreign import data ObjectStore :: Type 80 | 81 | 82 | -- | A cursor is used to iterate over a range of records in an index or an object store 83 | -- | in a specific direction. A ValueCursor also holds the value corresponding to matching key. 84 | foreign import data ValueCursor :: Type 85 | 86 | 87 | -- | A cursor has a direction that determines whether it moves in monotonically 88 | -- | increasing or decreasing order of the record keys when iterated, and if it 89 | -- | skips duplicated values when iterating indexes. 90 | -- | The direction of a cursor also determines if the cursor initial position is at 91 | -- | the start of its source or at its end. 92 | data CursorDirection 93 | = Next 94 | | NextUnique 95 | | Prev 96 | | PrevUnique 97 | 98 | 99 | -- | If the source of a cursor is an object store, the effective object store of 100 | -- | the cursor is that object store and the effective key of the cursor is the 101 | -- | cursor’s position. If the source of a cursor is an index, the effective object 102 | -- | store of the cursor is that index’s referenced object store and the effective key 103 | -- | is the cursor’s object store position. 104 | data CursorSource 105 | = ObjectStore ObjectStore 106 | | Index Index 107 | 108 | 109 | -- | A key path is a list of strings that defines how to extract a key from a value. 110 | -- | A valid key path is one of: 111 | -- | 112 | -- | - An empty list. 113 | -- | - An singleton identifier, which is a string matching the IdentifierName production from the ECMAScript Language Specification [ECMA-262]. 114 | -- | - A singleton string consisting of two or more identifiers separated by periods (U+002E FULL STOP). 115 | -- | - A non-empty list containing only strings conforming to the above requirements. 116 | type KeyPath = Array String 117 | 118 | 119 | -- | A transaction has a mode that determines which types of interactions can be performed 120 | -- | upon that transaction. The mode is set when the transaction is created and remains 121 | -- | fixed for the life of the transaction. 122 | data TransactionMode 123 | = ReadOnly -- ^ The transaction is only allowed to read data. 124 | | ReadWrite -- ^ The transaction is allowed to read, modify and delete data from existing object stores 125 | | VersionChange -- ^ The transaction is allowed to read, modify and delete data from existing object stores, and can also create and remove object stores and indexes. 126 | 127 | 128 | -------------------- 129 | -- Classes 130 | -- 131 | 132 | -- | A concrete cursor not only shares IDBCursor properties, but also some 133 | -- | specific attributes (see KeyCursor or CursorWithValue). 134 | class (IDBCursor cursor) <= IDBConcreteCursor cursor 135 | 136 | 137 | -- | Cursor objects implement the IDBCursor interface. 138 | -- | There is only ever one IDBCursor instance representing a given cursor. 139 | -- | There is no limit on how many cursors can be used at the same time. 140 | class IDBCursor cursor 141 | 142 | 143 | -- | The IDBDatabase interface represents a connection to a database. 144 | class IDBDatabase db 145 | 146 | 147 | -- | The IDBIndex interface represents an index handle. 148 | -- | Any of these methods throw an "TransactionInactiveError" DOMException 149 | -- | if called when the transaction is not active. 150 | class IDBIndex index 151 | 152 | 153 | -- | The IDBKeyRange interface represents a key range. 154 | class IDBKeyRange range 155 | 156 | 157 | -- | The IDBtransaction interface. 158 | class IDBTransaction tx 159 | 160 | 161 | -- | The IDBObjectStore interface represents an object store handle. 162 | class IDBObjectStore store 163 | 164 | 165 | -------------------- 166 | -- INSTANCES 167 | -- 168 | 169 | instance idbCursorKeyCursor :: IDBCursor KeyCursor 170 | 171 | 172 | instance idbCursorValueCursor :: IDBCursor ValueCursor 173 | 174 | 175 | instance idbConcreteCursorKeyCursor :: IDBConcreteCursor KeyCursor 176 | 177 | 178 | instance idbConcreteCursorValueCursor :: IDBConcreteCursor ValueCursor 179 | 180 | 181 | instance idbDatabaseDatabase :: IDBDatabase Database 182 | 183 | 184 | instance idbIndexIndex :: IDBIndex Index 185 | 186 | 187 | instance idbIndexObjectStore :: IDBIndex ObjectStore 188 | 189 | 190 | instance idbKeyRangeKeyRange :: IDBKeyRange KeyRange 191 | 192 | 193 | instance idbObjectStoreObjectStore :: IDBObjectStore ObjectStore 194 | 195 | 196 | instance idbTransactionTransaction :: IDBTransaction Transaction 197 | 198 | 199 | instance showKeyCursor :: Show KeyCursor where 200 | show = _showCursor 201 | 202 | 203 | instance showValueCursor :: Show ValueCursor where 204 | show = _showCursor 205 | 206 | 207 | instance showIndex :: Show Index where 208 | show = _showIndex 209 | 210 | 211 | instance showKeyRange :: Show KeyRange where 212 | show = _showKeyRange 213 | 214 | 215 | instance showObjectStore :: Show ObjectStore where 216 | show = _showObjectStore 217 | 218 | 219 | instance showTransaction :: Show Transaction where 220 | show = _showTransaction 221 | 222 | 223 | instance showDatabase :: Show Database where 224 | show = _showDatabase 225 | 226 | 227 | instance showCursorDirection :: Show CursorDirection where 228 | show x = 229 | case x of 230 | Next -> "next" 231 | NextUnique -> "nextunique" 232 | Prev -> "prev" 233 | PrevUnique -> "prevunique" 234 | 235 | 236 | instance showTransactionMode :: Show TransactionMode where 237 | show x = 238 | case x of 239 | ReadOnly -> "readonly" 240 | ReadWrite -> "readwrite" 241 | VersionChange -> "versionchange" 242 | 243 | 244 | instance readCursorDirection :: Read CursorDirection where 245 | read s = 246 | case s of 247 | "next" -> Just Next 248 | "nextunique" -> Just NextUnique 249 | "prev" -> Just Prev 250 | "prevunique" -> Just PrevUnique 251 | _ -> Nothing 252 | 253 | 254 | instance readTransactionMode :: Read TransactionMode where 255 | read s = 256 | case s of 257 | "readonly" -> Just ReadOnly 258 | "readwrite" -> Just ReadWrite 259 | "versionchange" -> Just VersionChange 260 | _ -> Nothing 261 | 262 | 263 | -------------------- 264 | -- FFI 265 | -- 266 | 267 | foreign import _showCursor 268 | :: forall cursor 269 | . cursor 270 | -> String 271 | 272 | 273 | foreign import _showDatabase 274 | :: Database 275 | -> String 276 | 277 | 278 | foreign import _showIndex 279 | :: Index 280 | -> String 281 | 282 | 283 | foreign import _showKeyRange 284 | :: KeyRange 285 | -> String 286 | 287 | 288 | foreign import _showObjectStore 289 | :: ObjectStore 290 | -> String 291 | 292 | 293 | foreign import _showTransaction 294 | :: Transaction 295 | -> String 296 | -------------------------------------------------------------------------------- /test/Main.purs: -------------------------------------------------------------------------------- 1 | module Test.Main where 2 | 3 | import Prelude 4 | 5 | import Control.Monad.Aff (Aff, launchAff, launchAff_, forkAff, delay, attempt) 6 | import Control.Monad.Aff.AVar (AVAR, AVar, makeVar, makeEmptyVar, readVar, putVar, takeVar) 7 | import Control.Monad.Aff.Console (log) 8 | import Control.Monad.Eff (Eff) 9 | import Control.Monad.Eff.Class (liftEff) 10 | import Control.Monad.Eff.Exception (EXCEPTION, name) 11 | import Control.Monad.Eff.Now (NOW, now) 12 | import Data.Date as Date 13 | import Data.DateTime as DateTime 14 | import Data.DateTime (DateTime(..), Time(..)) 15 | import Data.DateTime.Instant (toDateTime) 16 | import Data.Either (Either(..)) 17 | import Data.Enum (toEnum) 18 | import Data.Maybe (Maybe(..), isNothing, maybe) 19 | import Data.Array (head, drop) 20 | import Data.Time.Duration (Milliseconds(..)) 21 | import Data.Traversable (traverse) 22 | import Data.Tuple (Tuple(..), uncurry) 23 | import Test.Spec (describe, describeOnly, it) 24 | import Test.Spec.Assertions (shouldEqual, fail) 25 | import Test.Spec.Mocha (MOCHA, runMocha) 26 | 27 | import Database.IndexedDB.Core 28 | import Database.IndexedDB.IDBKey 29 | import Database.IndexedDB.IDBCursor as IDBCursor 30 | import Database.IndexedDB.IDBDatabase as IDBDatabase 31 | import Database.IndexedDB.IDBFactory as IDBFactory 32 | import Database.IndexedDB.IDBIndex as IDBIndex 33 | import Database.IndexedDB.IDBKeyRange as IDBKeyRange 34 | import Database.IndexedDB.IDBObjectStore as IDBObjectStore 35 | import Database.IndexedDB.IDBTransaction as IDBTransaction 36 | 37 | 38 | infixr 7 Tuple as :+: 39 | 40 | launchAff' :: forall a e. Aff e a -> Eff e Unit 41 | launchAff' = 42 | launchAff_ 43 | 44 | 45 | modifyVar :: forall eff a. (a -> a) -> AVar a -> Aff (avar :: AVAR | eff) Unit 46 | modifyVar fn v = do 47 | val <- takeVar v 48 | putVar (fn val) v 49 | 50 | 51 | -- main :: forall eff. Eff (now :: NOW, mocha :: MOCHA, idb :: IDB, exception :: EXCEPTION, avar :: AVAR | eff) Unit 52 | main = runMocha do 53 | describe "IDBFactory" do 54 | let 55 | tearDown name version db = do 56 | IDBDatabase.close db 57 | version' <- IDBFactory.deleteDatabase name 58 | version' `shouldEqual` version 59 | 60 | it "open default" do 61 | let name = "db-default" 62 | version = 1 63 | db <- IDBFactory.open name Nothing 64 | { onUpgradeNeeded : Nothing 65 | , onBlocked : Nothing 66 | } 67 | IDBDatabase.name db `shouldEqual` name 68 | tearDown name version db 69 | 70 | 71 | it "open specific version" do 72 | let name = "db-specific" 73 | version = 14 74 | db <- IDBFactory.open name (Just version) 75 | { onUpgradeNeeded : Nothing 76 | , onBlocked : Nothing 77 | } 78 | IDBDatabase.name db `shouldEqual` name 79 | tearDown name version db 80 | 81 | 82 | it "open specific version -> close -> open latest" do 83 | let name = "db-latest" 84 | version = 14 85 | db01 <- IDBFactory.open name (Just version) 86 | { onUpgradeNeeded : Nothing 87 | , onBlocked : Nothing 88 | } 89 | IDBDatabase.name db01 `shouldEqual` name 90 | IDBDatabase.close db01 91 | db02 <- IDBFactory.open name Nothing 92 | { onUpgradeNeeded : Nothing 93 | , onBlocked : Nothing 94 | } 95 | tearDown name version db02 96 | 97 | it "open + onUpgradeNeed" do 98 | let name = "db-upgrade-needed" 99 | version = 1 100 | callback (Tuple varName varVersion) db _ { oldVersion } = do 101 | _ <- launchAff $ modifyVar (const $ IDBDatabase.name db) varName 102 | _ <- launchAff $ modifyVar (const $ oldVersion) varVersion 103 | pure unit 104 | varName <- makeVar "-" 105 | varVersion <- makeVar (-1) 106 | db <- IDBFactory.open name Nothing 107 | { onUpgradeNeeded : Just (callback (Tuple varName varVersion)) 108 | , onBlocked : Nothing 109 | } 110 | name' <- readVar varName 111 | version' <- readVar varVersion 112 | name' `shouldEqual` name 113 | version' `shouldEqual` 0 114 | tearDown name version db 115 | 116 | it "open + onBlocked" do 117 | let name = "db-blocked" 118 | version = 14 119 | callback var = do 120 | _ <- launchAff $ modifyVar (const $ "db-blocked") var 121 | pure unit 122 | 123 | var <- makeVar "-" 124 | db01 <- IDBFactory.open name Nothing 125 | { onUpgradeNeeded : Nothing 126 | , onBlocked : Nothing 127 | } 128 | _ <- forkAff do 129 | delay (Milliseconds 100.0) 130 | IDBDatabase.close db01 131 | 132 | db02 <- IDBFactory.open name (Just version) 133 | { onUpgradeNeeded : Nothing 134 | , onBlocked : Just (callback var) 135 | } 136 | name' <- readVar var 137 | name' `shouldEqual` name 138 | tearDown name version db02 139 | 140 | describe "IDBKeyRange" do 141 | it "only(int)" do 142 | let key = 14 143 | range = IDBKeyRange.only key 144 | IDBKeyRange.includes range (toKey key) `shouldEqual` true 145 | 146 | it "only(string)" do 147 | let key = "patate" 148 | range = IDBKeyRange.only key 149 | IDBKeyRange.includes range (toKey key) `shouldEqual` true 150 | 151 | it "only(float)" do 152 | let key = 14.42 153 | range = IDBKeyRange.only key 154 | IDBKeyRange.includes range (toKey key) `shouldEqual` true 155 | 156 | it "only(date)" do 157 | let mkey = DateTime 158 | <$> (Date.canonicalDate <$> toEnum 2017 <*> toEnum 6 <*> toEnum 23) 159 | <*> (Time <$> toEnum 17 <*> toEnum 59 <*> toEnum 34 <*> toEnum 42) 160 | 161 | case mkey of 162 | Nothing -> 163 | fail "unable to create datetime" 164 | Just key -> do 165 | let range = IDBKeyRange.only key 166 | IDBKeyRange.includes range (toKey key) `shouldEqual` true 167 | 168 | it "only([int])" do 169 | let key = [14, 42] 170 | range = IDBKeyRange.only key 171 | IDBKeyRange.includes range (toKey key) `shouldEqual` true 172 | 173 | it "only([string])" do 174 | let key = ["patate", "autruche"] 175 | range = IDBKeyRange.only key 176 | IDBKeyRange.includes range (toKey key) `shouldEqual` true 177 | 178 | it "lowerBound(14, open)" do 179 | let key = 14 180 | open = true 181 | range = IDBKeyRange.lowerBound key open 182 | IDBKeyRange.includes range (toKey (key + 1)) `shouldEqual` true 183 | IDBKeyRange.includes range (toKey key) `shouldEqual` (not open) 184 | IDBKeyRange.includes range (toKey (key - 1)) `shouldEqual` false 185 | 186 | it "lowerBound(14, close)" do 187 | let key = 14 188 | open = false 189 | range = IDBKeyRange.lowerBound key open 190 | IDBKeyRange.includes range (toKey (key + 1)) `shouldEqual` true 191 | IDBKeyRange.includes range (toKey key) `shouldEqual` (not open) 192 | IDBKeyRange.includes range (toKey (key - 1)) `shouldEqual` false 193 | 194 | it "upperBound(14, open)" do 195 | let key = 14 196 | open = true 197 | range = IDBKeyRange.upperBound key open 198 | IDBKeyRange.includes range (toKey (key + 1)) `shouldEqual` false 199 | IDBKeyRange.includes range (toKey key) `shouldEqual` (not open) 200 | IDBKeyRange.includes range (toKey (key - 1)) `shouldEqual` true 201 | 202 | it "upperBound(14, close)" do 203 | let key = 14 204 | open = false 205 | range = IDBKeyRange.upperBound key open 206 | IDBKeyRange.includes range (toKey (key + 1)) `shouldEqual` false 207 | IDBKeyRange.includes range (toKey key) `shouldEqual` (not open) 208 | IDBKeyRange.includes range (toKey (key - 1)) `shouldEqual` true 209 | 210 | it "bound(42, 14, open, open) => Nothing" do 211 | let lower = 42 212 | upper = 14 213 | lowerOpen = true 214 | upperOpen = true 215 | mrange = IDBKeyRange.bound { lower, upper, lowerOpen, upperOpen } 216 | isNothing mrange `shouldEqual` true 217 | 218 | it "bound(14, 42, open, open)" do 219 | let lower = 14 220 | upper = 42 221 | lowerOpen = true 222 | upperOpen = true 223 | mrange = IDBKeyRange.bound { lower, upper, lowerOpen, upperOpen } 224 | case mrange of 225 | Nothing -> 226 | fail "invalid range provided" 227 | Just range -> do 228 | IDBKeyRange.includes range (toKey (lower + 1)) `shouldEqual` true 229 | IDBKeyRange.includes range (toKey (upper - 1)) `shouldEqual` true 230 | IDBKeyRange.includes range (toKey (lower - 1)) `shouldEqual` false 231 | IDBKeyRange.includes range (toKey (upper + 1)) `shouldEqual` false 232 | IDBKeyRange.includes range (toKey lower) `shouldEqual` (not lowerOpen) 233 | IDBKeyRange.includes range (toKey upper) `shouldEqual` (not upperOpen) 234 | 235 | it "bound(14, 42, open, close)" do 236 | let lower = 14 237 | upper = 42 238 | lowerOpen = true 239 | upperOpen = false 240 | mrange = IDBKeyRange.bound { lower, upper, lowerOpen, upperOpen } 241 | case mrange of 242 | Nothing -> 243 | fail "invalid range provided" 244 | Just range -> do 245 | IDBKeyRange.includes range (toKey (lower + 1)) `shouldEqual` true 246 | IDBKeyRange.includes range (toKey (upper - 1)) `shouldEqual` true 247 | IDBKeyRange.includes range (toKey (lower - 1)) `shouldEqual` false 248 | IDBKeyRange.includes range (toKey (upper + 1)) `shouldEqual` false 249 | IDBKeyRange.includes range (toKey lower) `shouldEqual` (not lowerOpen) 250 | IDBKeyRange.includes range (toKey upper) `shouldEqual` (not upperOpen) 251 | 252 | it "bound(14, 42, close, open)" do 253 | let lower = 14 254 | upper = 42 255 | lowerOpen = false 256 | upperOpen = true 257 | mrange = IDBKeyRange.bound { lower, upper, lowerOpen, upperOpen } 258 | case mrange of 259 | Nothing -> 260 | fail "invalid range provided" 261 | Just range -> do 262 | IDBKeyRange.includes range (toKey (lower + 1)) `shouldEqual` true 263 | IDBKeyRange.includes range (toKey (upper - 1)) `shouldEqual` true 264 | IDBKeyRange.includes range (toKey (lower - 1)) `shouldEqual` false 265 | IDBKeyRange.includes range (toKey (upper + 1)) `shouldEqual` false 266 | IDBKeyRange.includes range (toKey lower) `shouldEqual` (not lowerOpen) 267 | IDBKeyRange.includes range (toKey upper) `shouldEqual` (not upperOpen) 268 | 269 | it "bound(14, 42, close, close)" do 270 | let lower = 14 271 | upper = 42 272 | lowerOpen = false 273 | upperOpen = false 274 | mrange = IDBKeyRange.bound { lower, upper, lowerOpen, upperOpen } 275 | case mrange of 276 | Nothing -> 277 | fail "invalid range provided" 278 | Just range -> do 279 | IDBKeyRange.includes range (toKey (lower + 1)) `shouldEqual` true 280 | IDBKeyRange.includes range (toKey (upper - 1)) `shouldEqual` true 281 | IDBKeyRange.includes range (toKey (lower - 1)) `shouldEqual` false 282 | IDBKeyRange.includes range (toKey (upper + 1)) `shouldEqual` false 283 | IDBKeyRange.includes range (toKey lower) `shouldEqual` (not lowerOpen) 284 | IDBKeyRange.includes range (toKey upper) `shouldEqual` (not upperOpen) 285 | 286 | it "can access attributes of a range" do 287 | let range = IDBKeyRange.lowerBound 14 false 288 | IDBKeyRange.lower range `shouldEqual` (Just $ toKey 14) 289 | IDBKeyRange.upper range `shouldEqual` none 290 | IDBKeyRange.lowerOpen range `shouldEqual` false 291 | IDBKeyRange.upperOpen range `shouldEqual` true 292 | 293 | describe "IDBDatabase" do 294 | let 295 | tearDown db = do 296 | IDBDatabase.close db 297 | _ <- IDBFactory.deleteDatabase (IDBDatabase.name db) 298 | pure unit 299 | 300 | setup storeParams = do 301 | let onUpgradeNeeded var db _ _ = launchAff' do 302 | store <- IDBDatabase.createObjectStore db "store" storeParams 303 | _ <- putVar { db, store } var 304 | pure unit 305 | 306 | var <- makeEmptyVar 307 | db <- IDBFactory.open "db" Nothing 308 | { onUpgradeNeeded : Just (onUpgradeNeeded var) 309 | , onBlocked : Nothing 310 | } 311 | 312 | takeVar var 313 | 314 | it "createObjectStore (keyPath: [], autoIncrement: true)" do 315 | { db, store } <- setup { keyPath: [], autoIncrement: true } 316 | IDBObjectStore.name store `shouldEqual` "store" 317 | IDBObjectStore.keyPath store `shouldEqual` [] 318 | IDBObjectStore.autoIncrement store `shouldEqual` true 319 | IDBObjectStore.indexNames store `shouldEqual` [] 320 | tearDown db 321 | 322 | it "createObjectStore (keyPath: [\"patate\"], autoIncrement: true)" do 323 | { db, store } <- setup { keyPath: ["patate"], autoIncrement: true } 324 | IDBObjectStore.name store `shouldEqual` "store" 325 | IDBObjectStore.keyPath store `shouldEqual` ["patate"] 326 | IDBObjectStore.autoIncrement store `shouldEqual` true 327 | IDBObjectStore.indexNames store `shouldEqual` [] 328 | tearDown db 329 | 330 | it "createObjectStore (keyPath: [\"a\", \"b\"], autoIncrement: false)" do 331 | { db, store } <- setup { keyPath: ["patate", "autruche"], autoIncrement: false } 332 | IDBObjectStore.name store `shouldEqual` "store" 333 | IDBObjectStore.keyPath store `shouldEqual` ["patate", "autruche"] 334 | IDBObjectStore.autoIncrement store `shouldEqual` false 335 | IDBObjectStore.indexNames store `shouldEqual` [] 336 | tearDown db 337 | 338 | it "deleteObjectStore" do 339 | let onUpgradeNeeded var db _ _ = launchAff' do 340 | _ <- IDBDatabase.deleteObjectStore db "store" 341 | putVar true var 342 | 343 | var <- makeEmptyVar 344 | { db, store } <- setup IDBDatabase.defaultParameters 345 | IDBDatabase.close db 346 | db' <- IDBFactory.open "db" (Just 999) { onUpgradeNeeded : Just (onUpgradeNeeded var) 347 | , onBlocked : Nothing 348 | } 349 | deleted <- takeVar var 350 | deleted `shouldEqual` true 351 | tearDown db' 352 | 353 | 354 | describe "IDBObjectStore" do 355 | let 356 | tearDown db = do 357 | IDBDatabase.close db 358 | _ <- IDBFactory.deleteDatabase (IDBDatabase.name db) 359 | pure unit 360 | 361 | setup { storeParams, onUpgradeNeeded } = do 362 | let onUpgradeNeeded' var db _ _ = launchAff' do 363 | store <- IDBDatabase.createObjectStore db "store" storeParams 364 | liftEff $ maybe (pure unit) id (onUpgradeNeeded <*> pure db <*> pure store) 365 | putVar { db, store } var 366 | 367 | var <- makeEmptyVar 368 | db <- IDBFactory.open "db" Nothing 369 | { onUpgradeNeeded : Just (onUpgradeNeeded' var) 370 | , onBlocked : Nothing 371 | } 372 | 373 | takeVar var 374 | 375 | it "add()" do 376 | date <- liftEff $ toDateTime <$> now 377 | { db } <- setup 378 | { storeParams: { autoIncrement: true, keyPath: [] } 379 | , onUpgradeNeeded: Just $ \_ store -> launchAff' do 380 | -- no key 381 | key <- IDBObjectStore.add store "patate" none 382 | (toKey 1) `shouldEqual` key 383 | 384 | -- int key 385 | key' <- IDBObjectStore.add store "patate" (Just 14) 386 | (toKey 14) `shouldEqual` key' 387 | 388 | -- number key 389 | key'' <- IDBObjectStore.add store "patate" (Just 14.42) 390 | (toKey 14.42) `shouldEqual` key'' 391 | 392 | -- string key 393 | key''' <- IDBObjectStore.add store "patate" (Just "key") 394 | (toKey "key") `shouldEqual` key''' 395 | 396 | -- date key 397 | key'''' <- IDBObjectStore.add store "patate" (Just date) 398 | (toKey date) `shouldEqual` key'''' 399 | 400 | -- array key 401 | key''''' <- IDBObjectStore.add store "patate" (Just $ toKey [14, 42]) 402 | (toKey [14, 42]) `shouldEqual` key''''' 403 | } 404 | tearDown db 405 | 406 | it "clear()" do 407 | { db } <- setup 408 | { storeParams: IDBDatabase.defaultParameters 409 | , onUpgradeNeeded: Just $ \_ store -> launchAff' do 410 | key <- IDBObjectStore.add store "patate" (Just 14) 411 | _ <- IDBObjectStore.clear store 412 | val <- IDBObjectStore.get store (IDBKeyRange.only key) 413 | val `shouldEqual` (Nothing :: Maybe String) 414 | } 415 | tearDown db 416 | 417 | it "count()" do 418 | { db } <- setup 419 | { storeParams: IDBDatabase.defaultParameters 420 | , onUpgradeNeeded: Just $ \_ store -> launchAff' do 421 | _ <- IDBObjectStore.add store "patate" (Just 14) 422 | _ <- IDBObjectStore.add store "autruche" (Just 42) 423 | n <- IDBObjectStore.count store Nothing 424 | n `shouldEqual` 2 425 | } 426 | tearDown db 427 | 428 | it "getKey()" do 429 | { db } <- setup 430 | { storeParams: IDBDatabase.defaultParameters 431 | , onUpgradeNeeded: Just $ \_ store -> launchAff' do 432 | key <- IDBObjectStore.add store "patate" (Just 14) 433 | mkey <- IDBObjectStore.getKey store (IDBKeyRange.only 14) 434 | mkey `shouldEqual` (Just key) 435 | 436 | mkey' <- IDBObjectStore.getKey store (IDBKeyRange.only 42) 437 | mkey' `shouldEqual` none 438 | } 439 | tearDown db 440 | 441 | it "getAllKeys()" do 442 | { db } <- setup 443 | { storeParams: IDBDatabase.defaultParameters 444 | , onUpgradeNeeded: Just $ \_ store -> launchAff' do 445 | key1 <- IDBObjectStore.add store "patate" (Just 14) 446 | key2 <- IDBObjectStore.add store "autruche" (Just 42) 447 | key3 <- IDBObjectStore.add store 14 (Just 1337) 448 | 449 | -- no bounds 450 | keys <- IDBObjectStore.getAllKeys store Nothing Nothing 451 | keys `shouldEqual` [key1, key2, key3] 452 | 453 | -- lower bound 454 | keys' <- IDBObjectStore.getAllKeys store (Just $ IDBKeyRange.lowerBound 14 true) Nothing 455 | keys' `shouldEqual` [key2, key3] 456 | 457 | -- upper bound 458 | keys'' <- IDBObjectStore.getAllKeys store (Just $ IDBKeyRange.upperBound 42 false) Nothing 459 | keys'' `shouldEqual` [key1, key2] 460 | 461 | -- count 462 | keys''' <- IDBObjectStore.getAllKeys store (Just $ IDBKeyRange.lowerBound 1 true) (Just 2) 463 | keys''' `shouldEqual` [key1, key2] 464 | } 465 | tearDown db 466 | 467 | it "openCursor()" do 468 | let cb = 469 | { onSuccess : const $ pure unit 470 | , onError : show >>> fail >>> launchAff' 471 | , onComplete: pure unit 472 | } 473 | { db } <- setup 474 | { storeParams: IDBDatabase.defaultParameters 475 | , onUpgradeNeeded: Just $ \_ store -> launchAff' do 476 | _ <- IDBObjectStore.openCursor store Nothing Next cb 477 | _ <- IDBObjectStore.openCursor store Nothing NextUnique cb 478 | _ <- IDBObjectStore.openCursor store Nothing Prev cb 479 | _ <- IDBObjectStore.openCursor store Nothing PrevUnique cb 480 | _ <- IDBObjectStore.openCursor store (Just $ IDBKeyRange.upperBound 1 true) Next cb 481 | pure unit 482 | } 483 | tearDown db 484 | 485 | it "openKeyCursor()" do 486 | let cb = 487 | { onSuccess : const $ pure unit 488 | , onError : show >>> fail >>> launchAff' 489 | , onComplete: pure unit 490 | } 491 | { db } <- setup 492 | { storeParams: IDBDatabase.defaultParameters 493 | , onUpgradeNeeded: Just $ \_ store -> launchAff' do 494 | _ <- IDBObjectStore.openKeyCursor store Nothing Next cb 495 | _ <- IDBObjectStore.openKeyCursor store Nothing NextUnique cb 496 | _ <- IDBObjectStore.openKeyCursor store Nothing Prev cb 497 | _ <- IDBObjectStore.openKeyCursor store Nothing PrevUnique cb 498 | _ <- IDBObjectStore.openKeyCursor store (Just $ IDBKeyRange.lowerBound 1 true) Next cb 499 | pure unit 500 | } 501 | tearDown db 502 | 503 | describe "IDBIndex" do 504 | let 505 | tearDown db = do 506 | IDBDatabase.close db 507 | _ <- IDBFactory.deleteDatabase (IDBDatabase.name db) 508 | pure unit 509 | 510 | setup :: forall value e e'. 511 | { storeParams :: { keyPath :: Array String, autoIncrement :: Boolean } 512 | , indexParams :: { unique :: Boolean, multiEntry :: Boolean } 513 | , values :: Array (Tuple value (Maybe Key)) 514 | , keyPath :: Array String 515 | , onUpgradeNeeded :: Maybe (Database -> Transaction -> Index -> Eff (idb :: IDB, avar :: AVAR, exception :: EXCEPTION | e') Unit) 516 | } -> Aff (idb :: IDB, avar :: AVAR | e) { db :: Database, index :: Index, store :: ObjectStore } 517 | setup { storeParams, indexParams, values, keyPath, onUpgradeNeeded } = do 518 | let onUpgradeNeeded' var db tx _ = launchAff' do 519 | store <- IDBDatabase.createObjectStore db "store" storeParams 520 | _ <- traverse (uncurry (IDBObjectStore.add store)) values 521 | index <- IDBObjectStore.createIndex store "index" keyPath indexParams 522 | liftEff $ maybe (pure unit) id (onUpgradeNeeded <*> pure db <*> pure tx <*> pure index) 523 | putVar { db, index, store } var 524 | 525 | var <- makeEmptyVar 526 | db <- IDBFactory.open "db" Nothing 527 | { onUpgradeNeeded : Just (onUpgradeNeeded' var) 528 | , onBlocked : Nothing 529 | } 530 | takeVar var 531 | 532 | 533 | it "returns an IDBIndex and the properties are set correctly" do 534 | { db, index } <- setup 535 | { storeParams : IDBDatabase.defaultParameters 536 | , indexParams : IDBObjectStore.defaultParameters 537 | , onUpgradeNeeded : Nothing 538 | , keyPath : [] 539 | , values : [] 540 | } 541 | IDBIndex.name index `shouldEqual` "index" 542 | IDBIndex.keyPath index `shouldEqual` [] 543 | IDBIndex.unique index `shouldEqual` IDBObjectStore.defaultParameters.unique 544 | IDBIndex.multiEntry index `shouldEqual` IDBObjectStore.defaultParameters.multiEntry 545 | tearDown db 546 | 547 | it "attempt to create an index that requires unique values on an object store already contains duplicates" do 548 | let onAbort var = launchAff' (putVar true var) 549 | txVar <- makeEmptyVar 550 | dbVar <- makeEmptyVar 551 | res <- attempt $ setup 552 | { storeParams : IDBDatabase.defaultParameters 553 | , indexParams : { unique : true 554 | , multiEntry : false 555 | } 556 | , keyPath : ["indexedProperty"] 557 | , values : [ { indexedProperty: "bar" } :+: (Just $ toKey 1) 558 | , { indexedProperty: "bar" } :+: (Just $ toKey 2) 559 | ] 560 | , onUpgradeNeeded : Just $ \db tx _ -> launchAff' do 561 | IDBTransaction.onAbort tx (onAbort txVar) 562 | IDBDatabase.onAbort db (onAbort dbVar) 563 | } 564 | case res of 565 | Right _ -> 566 | fail "expected abort" 567 | _ -> do 568 | shouldEqual true =<< takeVar txVar 569 | shouldEqual true =<< takeVar dbVar 570 | 571 | 572 | it "the index is usable right after being made" do 573 | { db } <- setup 574 | { storeParams : { keyPath : ["key"] 575 | , autoIncrement : false 576 | } 577 | , indexParams : IDBObjectStore.defaultParameters 578 | , keyPath : ["indexedProperty"] 579 | , values : [ { key: "key1", indexedProperty: "indexed_1" } :+: Nothing 580 | , { key: "key2", indexedProperty: "indexed_2" } :+: Nothing 581 | , { key: "key3", indexedProperty: "indexed_3" } :+: Nothing 582 | ] 583 | , onUpgradeNeeded : Just $ \_ _ index -> launchAff' do 584 | val <- IDBIndex.get index (IDBKeyRange.only "indexed_2") 585 | ((\r -> r.key) <$> val) `shouldEqual` (Just $ toKey "key2") 586 | } 587 | tearDown db 588 | 589 | it "empty keyPath" do 590 | { db } <- setup 591 | { storeParams : IDBDatabase.defaultParameters 592 | , indexParams : IDBObjectStore.defaultParameters 593 | , keyPath : [] 594 | , values : [ "object_1" :+: (Just $ toKey 1) 595 | , "object_2" :+: (Just $ toKey 2) 596 | , "object_3" :+: (Just $ toKey 3) 597 | ] 598 | , onUpgradeNeeded : Just $ \_ _ index -> launchAff' do 599 | val <- IDBIndex.get index (IDBKeyRange.only "object_3") 600 | val `shouldEqual` (Just "object_3") 601 | } 602 | tearDown db 603 | 604 | it "index can be valid keys [date]" do 605 | date <- liftEff $ toDateTime <$> now 606 | { db } <- setup 607 | { storeParams : { keyPath: ["key"] 608 | , autoIncrement: false 609 | } 610 | , indexParams : IDBObjectStore.defaultParameters 611 | , keyPath : ["i"] 612 | , values : [ { key: "date", i: (toKey date) } :+: Nothing 613 | ] 614 | , onUpgradeNeeded : Just $ \_ _ index -> launchAff' do 615 | val <- IDBIndex.get index (IDBKeyRange.only date) 616 | ((\r -> r.key) <$> val) `shouldEqual` (Just "date") 617 | } 618 | tearDown db 619 | 620 | it "index can be valid keys [num]" do 621 | let num = 14 622 | { db } <- setup 623 | { storeParams : { keyPath: ["key"] 624 | , autoIncrement: false 625 | } 626 | , indexParams : IDBObjectStore.defaultParameters 627 | , keyPath : ["i"] 628 | , values : [ { key: "num", i: (toKey num) } :+: Nothing 629 | ] 630 | , onUpgradeNeeded : Just $ \_ _ index -> launchAff' do 631 | val <- IDBIndex.get index (IDBKeyRange.only num) 632 | ((\r -> r.key) <$> val) `shouldEqual` (Just "num") 633 | } 634 | tearDown db 635 | 636 | it "index can be valid keys [array]" do 637 | let array = ["patate", "autruche"] 638 | { db } <- setup 639 | { storeParams : { keyPath: ["key"] 640 | , autoIncrement: false 641 | } 642 | , indexParams : IDBObjectStore.defaultParameters 643 | , keyPath : ["i"] 644 | , values : [ { key: "array", i: (toKey array) } :+: Nothing 645 | ] 646 | , onUpgradeNeeded : Just $ \_ _ index -> launchAff' do 647 | val <- IDBIndex.get index (IDBKeyRange.only array) 648 | ((\r -> r.key) <$> val) `shouldEqual` (Just "array") 649 | } 650 | tearDown db 651 | 652 | it "openKeyCursor() - throw InvalidStateError on index deleted by aborted upgrade" do 653 | res <- attempt $ setup 654 | { storeParams : { keyPath: ["key"], autoIncrement: false } 655 | , indexParams : IDBObjectStore.defaultParameters 656 | , keyPath : ["indexedProperty"] 657 | , values : [ { key: 14, indexedProperty: "patate" } :+: Nothing 658 | ] 659 | , onUpgradeNeeded : Just $ \db tx index -> launchAff' do 660 | let cb = 661 | { onSuccess : const $ pure unit 662 | , onError : show >>> fail >>> launchAff' 663 | , onComplete: pure unit 664 | } 665 | IDBTransaction.onAbort tx (pure unit) 666 | IDBDatabase.onAbort db (pure unit) 667 | IDBTransaction.abort tx 668 | cursor <- attempt $ IDBIndex.openKeyCursor index Nothing Next cb 669 | case cursor of 670 | Right _ -> fail "expected InvalidStateError" 671 | Left err -> name err `shouldEqual` "InvalidStateError" 672 | } 673 | case res of 674 | Right _ -> fail "expected InvalidStateError" 675 | _ -> pure unit 676 | 677 | it "openKeyCursor() - throw TransactionInactiveError on aborted transaction" do 678 | { db } <- setup 679 | { storeParams : { keyPath: ["key"], autoIncrement: false } 680 | , indexParams : IDBObjectStore.defaultParameters 681 | , keyPath : ["indexedProperty"] 682 | , values : [ { key: 14, indexedProperty: "patate" } :+: Nothing 683 | ] 684 | , onUpgradeNeeded : Nothing 685 | } 686 | let cb = 687 | { onSuccess : const $ pure unit 688 | , onError : show >>> fail >>> launchAff' 689 | , onComplete: pure unit 690 | } 691 | tx <- IDBDatabase.transaction db ["store"] ReadOnly 692 | store <- IDBTransaction.objectStore tx "store" 693 | index <- IDBObjectStore.index store "index" 694 | IDBTransaction.onAbort tx (pure unit) 695 | IDBTransaction.abort tx 696 | cursor <- attempt $ IDBIndex.openKeyCursor index Nothing Next cb 697 | case cursor of 698 | Right _ -> fail "expected TransactionInactiveError" 699 | Left err -> name err `shouldEqual` "TransactionInactiveError" 700 | 701 | tearDown db 702 | 703 | it "openKeyCursor() - throw InvalidStateError when the index is deleted" do 704 | { db } <- setup 705 | { storeParams : { keyPath: ["key"], autoIncrement: false } 706 | , indexParams : IDBObjectStore.defaultParameters 707 | , keyPath : ["indexedProperty"] 708 | , values : [ { key: 14, indexedProperty: "patate" } :+: Nothing 709 | ] 710 | , onUpgradeNeeded : Just $ \db tx index -> launchAff' do 711 | let cb = 712 | { onSuccess : const $ pure unit 713 | , onError : show >>> fail >>> launchAff' 714 | , onComplete: pure unit 715 | } 716 | store <- IDBTransaction.objectStore tx "store" 717 | IDBObjectStore.deleteIndex store "index" 718 | cursor <- attempt $ IDBIndex.openKeyCursor index Nothing Next cb 719 | case cursor of 720 | Right _ -> fail "expected InvalidStateError" 721 | Left err -> name err `shouldEqual` "InvalidStateError" 722 | } 723 | 724 | tearDown db 725 | 726 | it "openCursor() - throw InvalidStateError on index deleted by aborted upgrade" do 727 | res <- attempt $ setup 728 | { storeParams : { keyPath: ["key"], autoIncrement: false } 729 | , indexParams : IDBObjectStore.defaultParameters 730 | , keyPath : ["indexedProperty"] 731 | , values : [ { key: 14, indexedProperty: "patate" } :+: Nothing 732 | ] 733 | , onUpgradeNeeded : Just $ \db tx index -> launchAff' do 734 | let cb = 735 | { onSuccess : const $ pure unit 736 | , onError : show >>> fail >>> launchAff' 737 | , onComplete: pure unit 738 | } 739 | IDBTransaction.onAbort tx (pure unit) 740 | IDBDatabase.onAbort db (pure unit) 741 | IDBTransaction.abort tx 742 | cursor <- attempt $ IDBIndex.openCursor index Nothing Next cb 743 | case cursor of 744 | Right _ -> fail "expected InvalidStateError" 745 | Left err -> name err `shouldEqual` "InvalidStateError" 746 | } 747 | case res of 748 | Right _ -> fail "expected InvalidStateError" 749 | _ -> pure unit 750 | 751 | it "openCursor() - throw TransactionInactiveError on aborted transaction" do 752 | { db } <- setup 753 | { storeParams : { keyPath: ["key"], autoIncrement: false } 754 | , indexParams : IDBObjectStore.defaultParameters 755 | , keyPath : ["indexedProperty"] 756 | , values : [ { key: 14, indexedProperty: "patate" } :+: Nothing 757 | ] 758 | , onUpgradeNeeded : Nothing 759 | } 760 | let cb = 761 | { onSuccess : const $ pure unit 762 | , onError : show >>> fail >>> launchAff' 763 | , onComplete: pure unit 764 | } 765 | tx <- IDBDatabase.transaction db ["store"] ReadOnly 766 | store <- IDBTransaction.objectStore tx "store" 767 | index <- IDBObjectStore.index store "index" 768 | IDBTransaction.onAbort tx (pure unit) 769 | IDBTransaction.abort tx 770 | cursor <- attempt $ IDBIndex.openCursor index Nothing Next cb 771 | case cursor of 772 | Right _ -> fail "expected TransactionInactiveError" 773 | Left err -> name err `shouldEqual` "TransactionInactiveError" 774 | 775 | tearDown db 776 | 777 | it "openCursor() - throw InvalidStateError when the index is deleted" do 778 | { db } <- setup 779 | { storeParams : { keyPath: ["key"], autoIncrement: false } 780 | , indexParams : IDBObjectStore.defaultParameters 781 | , keyPath : ["indexedProperty"] 782 | , values : [ { key: 14, indexedProperty: "patate" } :+: Nothing 783 | ] 784 | , onUpgradeNeeded : Just $ \db tx index -> launchAff' do 785 | let cb = 786 | { onSuccess : const $ pure unit 787 | , onError : show >>> fail >>> launchAff' 788 | , onComplete: pure unit 789 | } 790 | store <- IDBTransaction.objectStore tx "store" 791 | IDBObjectStore.deleteIndex store "index" 792 | cursor <- attempt $ IDBIndex.openCursor index Nothing Next cb 793 | case cursor of 794 | Right _ -> fail "expected InvalidStateError" 795 | Left err -> name err `shouldEqual` "InvalidStateError" 796 | } 797 | 798 | tearDown db 799 | 800 | it "getKey() - multiEntry - adding keys" do 801 | { db } <- setup 802 | { storeParams : IDBDatabase.defaultParameters 803 | , indexParams : { unique: false, multiEntry: true } 804 | , keyPath : ["name"] 805 | , values : [ { name: ["patate", "autruche"] } :+: (Just $ toKey 1) 806 | , { name: ["bob"] } :+: (Just $ toKey 2) 807 | ] 808 | , onUpgradeNeeded : Just $ \_ _ index -> launchAff' do 809 | key <- IDBIndex.getKey index (IDBKeyRange.only "patate") 810 | key `shouldEqual` (Just $ toKey 1) 811 | 812 | key' <- IDBIndex.getKey index (IDBKeyRange.only "autruche") 813 | key' `shouldEqual` (Just $ toKey 1) 814 | 815 | key'' <- IDBIndex.getKey index (IDBKeyRange.only "bob") 816 | key'' `shouldEqual` (Just $ toKey 2) 817 | } 818 | tearDown db 819 | 820 | 821 | it "get() - returns the record with the first key in the range" do 822 | { db } <- setup 823 | { storeParams : { keyPath: ["key"], autoIncrement: false } 824 | , indexParams : IDBObjectStore.defaultParameters 825 | , keyPath : ["indexedProperty"] 826 | , values : [ { key: 14, indexedProperty: "patate" } :+: Nothing 827 | , { key: 42, indexedProperty: "autruche" } :+: Nothing 828 | , { key: 1337, indexedProperty: "baguette" } :+: Nothing 829 | ] 830 | , onUpgradeNeeded : Nothing 831 | } 832 | 833 | tx <- IDBDatabase.transaction db ["store"] ReadOnly 834 | store <- IDBTransaction.objectStore tx "store" 835 | index <- IDBObjectStore.index store "index" 836 | val <- IDBIndex.get index (IDBKeyRange.lowerBound "autruche" false) 837 | ((\r -> r.key) <$> val) `shouldEqual` (Just 42) 838 | 839 | tearDown db 840 | 841 | describe "IDBCursor" do 842 | let 843 | tearDown db = do 844 | IDBDatabase.close db 845 | _ <- IDBFactory.deleteDatabase (IDBDatabase.name db) 846 | pure unit 847 | 848 | setup :: forall value e e'. 849 | { storeParams :: { keyPath :: Array String, autoIncrement :: Boolean } 850 | , indexParams :: { unique :: Boolean, multiEntry :: Boolean } 851 | , values :: Array (Tuple value (Maybe Key)) 852 | , keyPath :: Array String 853 | , onUpgradeNeeded :: Maybe (Database -> Transaction -> Index -> Eff (idb :: IDB, avar :: AVAR, exception :: EXCEPTION | e') Unit) 854 | } -> Aff (idb :: IDB, avar :: AVAR | e) { db :: Database, index :: Index, store :: ObjectStore } 855 | setup { storeParams, indexParams, values, keyPath, onUpgradeNeeded } = do 856 | let onUpgradeNeeded' var db tx _ = launchAff' do 857 | store <- IDBDatabase.createObjectStore db "store" storeParams 858 | _ <- traverse (uncurry (IDBObjectStore.add store)) values 859 | index <- IDBObjectStore.createIndex store "index" keyPath indexParams 860 | liftEff $ maybe (pure unit) id (onUpgradeNeeded <*> pure db <*> pure tx <*> pure index) 861 | putVar { db, index, store } var 862 | 863 | var <- makeEmptyVar 864 | db <- IDBFactory.open "db" Nothing 865 | { onUpgradeNeeded : Just (onUpgradeNeeded' var) 866 | , onBlocked : Nothing 867 | } 868 | takeVar var 869 | 870 | it "continue() - iterate to the next record" do 871 | { db } <- setup 872 | { storeParams : IDBDatabase.defaultParameters 873 | , indexParams : IDBObjectStore.defaultParameters 874 | , keyPath : [] 875 | , values : [ "cupcake" :+: (Just $ toKey 4) 876 | , "pancake" :+: (Just $ toKey 2) 877 | , "pie" :+: (Just $ toKey 1) 878 | , "pie" :+: (Just $ toKey 3) 879 | ] 880 | , onUpgradeNeeded: Nothing 881 | } 882 | let cb vdone vvals = 883 | { onComplete: launchAff' do 884 | putVar unit vdone 885 | 886 | , onError: \error -> launchAff' do 887 | fail $ "unexpected error: " <> show error 888 | 889 | , onSuccess: \cursor -> launchAff' do 890 | vals <- takeVar vvals 891 | pure (IDBCursor.value cursor) >>= shouldEqual (maybe "" _.v $ head vals) 892 | IDBCursor.primaryKey cursor >>= shouldEqual (maybe (toKey 0) _.k $ head vals) 893 | putVar (drop 1 vals) vvals 894 | IDBCursor.continue cursor none 895 | } 896 | vdone <- makeEmptyVar 897 | vvals <- makeVar 898 | [ { v: "pie" , k: toKey 1 } 899 | , { v: "pancake", k: toKey 2 } 900 | , { v: "pie" , k: toKey 3 } 901 | , { v: "cupcake", k: toKey 4 } 902 | ] 903 | tx <- IDBDatabase.transaction db ["store"] ReadOnly 904 | store <- IDBTransaction.objectStore tx "store" 905 | IDBObjectStore.openCursor store Nothing Next (cb vdone vvals) 906 | takeVar vdone 907 | tearDown db 908 | 909 | it "continue() - attempt to iterate in the wrong direction" do 910 | { db } <- setup 911 | { storeParams : IDBDatabase.defaultParameters 912 | , indexParams : IDBObjectStore.defaultParameters 913 | , keyPath : [] 914 | , values : [ "cupcake" :+: (Just $ toKey 4) 915 | , "pancake" :+: (Just $ toKey 2) 916 | , "pie" :+: (Just $ toKey 1) 917 | , "pie" :+: (Just $ toKey 3) 918 | ] 919 | , onUpgradeNeeded: Nothing 920 | } 921 | let cb vdone = 922 | { onComplete: launchAff' do 923 | fail $ "shouldn't complete" 924 | 925 | , onError: \error -> launchAff' do 926 | fail $ "unexpected error: " <> show error 927 | 928 | , onSuccess: \cursor -> launchAff' do 929 | res <- attempt $ IDBCursor.continue cursor (Just 1) 930 | case res of 931 | Left err -> do 932 | name err `shouldEqual` "DataError" 933 | putVar unit vdone 934 | Right _ -> do 935 | fail "expected continue to fail" 936 | } 937 | vdone <- makeEmptyVar 938 | tx <- IDBDatabase.transaction db ["store"] ReadOnly 939 | store <- IDBTransaction.objectStore tx "store" 940 | IDBObjectStore.openCursor store Nothing Next (cb vdone) 941 | takeVar vdone 942 | tearDown db 943 | 944 | 945 | it "advance() - iterate cursor number of times specified by count" do 946 | { db } <- setup 947 | { storeParams : { keyPath: ["pKey"], autoIncrement: false } 948 | , indexParams : IDBObjectStore.defaultParameters 949 | , keyPath : [] 950 | , values : [ { pKey: "pkey_0", iKey: "ikey_0" } :+: Nothing 951 | , { pKey: "pkey_1", iKey: "ikey_1" } :+: Nothing 952 | , { pKey: "pkey_2", iKey: "ikey_2" } :+: Nothing 953 | , { pKey: "pkey_3", iKey: "ikey_3" } :+: Nothing 954 | ] 955 | , onUpgradeNeeded: Nothing 956 | } 957 | let cb vdone vjump = 958 | { onComplete: launchAff' do 959 | putVar unit vdone 960 | 961 | , onError: \error -> launchAff' do 962 | fail $ "unexpected error: " <> show error 963 | 964 | , onSuccess: \cursor -> launchAff' do 965 | jump <- takeVar vjump 966 | if jump 967 | then do 968 | IDBCursor.advance cursor 3 969 | else do 970 | let value = IDBCursor.value cursor 971 | value.pKey `shouldEqual` "pkey_3" 972 | value.iKey `shouldEqual` "ikey_3" 973 | IDBCursor.continue cursor none 974 | putVar false vjump 975 | } 976 | vdone <- makeEmptyVar 977 | vjump <- makeVar true 978 | tx <- IDBDatabase.transaction db ["store"] ReadOnly 979 | store <- IDBTransaction.objectStore tx "store" 980 | IDBObjectStore.openCursor store Nothing Next (cb vdone vjump) 981 | takeVar vdone 982 | tearDown db 983 | 984 | it "delete() - remove a record from the object store" do 985 | { db } <- setup 986 | { storeParams : { keyPath: ["pKey"], autoIncrement: false } 987 | , indexParams : IDBObjectStore.defaultParameters 988 | , keyPath : [] 989 | , values : [ { pKey: "pkey_0", iKey: "ikey_0" } :+: Nothing 990 | , { pKey: "pkey_1", iKey: "ikey_1" } :+: Nothing 991 | , { pKey: "pkey_2", iKey: "ikey_2" } :+: Nothing 992 | , { pKey: "pkey_3", iKey: "ikey_3" } :+: Nothing 993 | ] 994 | , onUpgradeNeeded: Nothing 995 | } 996 | let cb vdone store = 997 | { onComplete: launchAff' do 998 | mval <- map _.pKey <$> IDBIndex.get store (IDBKeyRange.only "pkey_0") 999 | mval `shouldEqual` (Nothing :: Maybe String) 1000 | putVar unit vdone 1001 | 1002 | , onError: \error -> launchAff' do 1003 | fail $ "unexpected error: " <> show error 1004 | 1005 | , onSuccess: \cursor -> launchAff' do 1006 | let value = IDBCursor.value cursor 1007 | value.pKey `shouldEqual` "pkey_0" 1008 | IDBCursor.delete cursor 1009 | IDBCursor.advance cursor 4 1010 | } 1011 | vdone <- makeEmptyVar 1012 | tx <- IDBDatabase.transaction db ["store"] ReadWrite 1013 | store <- IDBTransaction.objectStore tx "store" 1014 | IDBObjectStore.openCursor store Nothing Next (cb vdone store) 1015 | takeVar vdone 1016 | tearDown db 1017 | 1018 | it "update() - modify a record in the object store" do 1019 | { db } <- setup 1020 | { storeParams : { keyPath: ["pKey"], autoIncrement: false } 1021 | , indexParams : IDBObjectStore.defaultParameters 1022 | , keyPath : [] 1023 | , values : [ { pKey: "pkey_0", iKey: "ikey_0" } :+: Nothing 1024 | ] 1025 | , onUpgradeNeeded: Nothing 1026 | } 1027 | let cb vdone store = 1028 | { onComplete: launchAff' do 1029 | mval <- map _.iKey <$> IDBIndex.get store (IDBKeyRange.only "pkey_0") 1030 | mval `shouldEqual` (Just "patate") 1031 | putVar unit vdone 1032 | 1033 | , onError: \error -> launchAff' do 1034 | fail $ "unexpected error: " <> show error 1035 | 1036 | , onSuccess: \cursor -> launchAff' do 1037 | let value = IDBCursor.value cursor 1038 | value.pKey `shouldEqual` "pkey_0" 1039 | key <- IDBCursor.update cursor { pKey: "pkey_0", iKey: "patate" } 1040 | key `shouldEqual` toKey "pkey_0" 1041 | IDBCursor.advance cursor 4 1042 | } 1043 | vdone <- makeEmptyVar 1044 | tx <- IDBDatabase.transaction db ["store"] ReadWrite 1045 | store <- IDBTransaction.objectStore tx "store" 1046 | IDBObjectStore.openCursor store Nothing Next (cb vdone store) 1047 | takeVar vdone 1048 | tearDown db 1049 | 1050 | it "update() - throw ReadOnlyError after update on ReadOnly transaction" do 1051 | { db } <- setup 1052 | { storeParams : { keyPath: ["pKey"], autoIncrement: false } 1053 | , indexParams : IDBObjectStore.defaultParameters 1054 | , keyPath : [] 1055 | , values : [ { pKey: "pkey_0", iKey: "ikey_0" } :+: Nothing 1056 | ] 1057 | , onUpgradeNeeded: Nothing 1058 | } 1059 | let cb vdone = 1060 | { onComplete: launchAff' do 1061 | fail $ "shouldn't complete" 1062 | 1063 | , onError: \error -> launchAff' do 1064 | fail $ "unexpected error: " <> show error 1065 | 1066 | , onSuccess: \cursor -> launchAff' do 1067 | res <- attempt $ IDBCursor.update cursor "patate" 1068 | case res of 1069 | Left err -> do 1070 | name err `shouldEqual` "ReadOnlyError" 1071 | putVar unit vdone 1072 | Right _ -> 1073 | fail $ "expected ReadOnlyError" 1074 | } 1075 | vdone <- makeEmptyVar 1076 | tx <- IDBDatabase.transaction db ["store"] ReadOnly 1077 | store <- IDBTransaction.objectStore tx "store" 1078 | IDBObjectStore.openCursor store Nothing Next (cb vdone) 1079 | takeVar vdone 1080 | tearDown db 1081 | --------------------------------------------------------------------------------