From e0307d4cbf35b4f27858f1a882a76d9089c0d5ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Lemaire?= Date: Wed, 27 Nov 2019 22:51:27 +0100 Subject: [PATCH] Added remote storage based on rest server --- .dockerignore | 2 + .vscode/settings.json | 3 + Dockerfile | 9 ++ README.md | 11 ++ dist/index.html | 11 ++ package-lock.json | 342 +++++++++++++++++++++++++++++++++++++++++- package.json | 9 ++ src/index.test.ts | 24 ++- src/index.ts | 22 ++- src/remote.test.ts | 33 ++++ src/remote.ts | 64 ++++++++ src/server.ts | 64 ++++++++ 12 files changed, 586 insertions(+), 8 deletions(-) create mode 100644 .dockerignore create mode 100644 .vscode/settings.json create mode 100644 Dockerfile create mode 100644 dist/index.html create mode 100644 src/remote.test.ts create mode 100644 src/remote.ts create mode 100644 src/server.ts diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..671215d --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +node_modules +.venv diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..55712c1 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "typescript.tsdk": "node_modules/typescript/lib" +} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..48662aa --- /dev/null +++ b/Dockerfile @@ -0,0 +1,9 @@ +FROM node + +ADD . /app +WORKDIR /app + +RUN npm install + +EXPOSE 5001 +CMD npm run storageserver diff --git a/README.md b/README.md index 00a3a52..1afa7da 100644 --- a/README.md +++ b/README.md @@ -42,9 +42,20 @@ const storage = tkStorage.getLocalStorage("myapp"); Use --- +To get a storage locally persistent (saved in browser data or on disk for Node.js): + ```javascript const storage = getLocalStorage("myapp"); await storage.get("key"); // => null await storage.set("key", "value"); await storage.get("key"); // => "value" ``` + +To get a storage remotely persistent (saved on a compliant server): + +```javascript +const storage = getRemoteStorage("myapp", "https://tk-storage.example.io/", { shared: true }); +await storage.get("key"); // => null +await storage.set("key", "value"); +await storage.get("key"); // => "value" +``` diff --git a/dist/index.html b/dist/index.html new file mode 100644 index 0000000..874c4a8 --- /dev/null +++ b/dist/index.html @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/package-lock.json b/package-lock.json index 27e0a2d..8d604c5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1155,12 +1155,61 @@ "@babel/types": "^7.3.0" } }, + "@types/body-parser": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.17.1.tgz", + "integrity": "sha512-RoX2EZjMiFMjZh9lmYrwgoP9RTpAjSHiJxdp4oidAQVO02T7HER3xj9UKue5534ULWeqVEkujhWcyvUce+d68w==", + "dev": true, + "requires": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "@types/connect": { + "version": "3.4.32", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.32.tgz", + "integrity": "sha512-4r8qa0quOvh7lGD0pre62CAb1oni1OO6ecJLGCezTmhQ8Fz50Arx9RUszryR8KlgK6avuSXvviL6yWyViQABOg==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-invOmosX0DqbpA+cE2yoHGUlF/blyf7nB0OGYBBiH27crcVm5NmFaZkLP4Ta1hGaesckCi5lVLlydNJCxkTOSg==", + "dev": true, + "requires": { + "@types/express": "*" + } + }, "@types/estree": { "version": "0.0.39", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", "dev": true }, + "@types/express": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.2.tgz", + "integrity": "sha512-5mHFNyavtLoJmnusB8OKJ5bshSzw+qkMIBAobLrIM48HJvunFva9mOa6aBwh64lBFyNwBbs0xiEFuj4eU/NjCA==", + "dev": true, + "requires": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "*", + "@types/serve-static": "*" + } + }, + "@types/express-serve-static-core": { + "version": "4.17.0", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.0.tgz", + "integrity": "sha512-Xnub7w57uvcBqFdIGoRg1KhNOeEj0vB6ykUM7uFWyxvbdE89GFyqgmUcanAriMr4YOxNFZBAWkfcWIb4WBPt3g==", + "dev": true, + "requires": { + "@types/node": "*", + "@types/range-parser": "*" + } + }, "@types/istanbul-lib-coverage": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz", @@ -1201,6 +1250,12 @@ "integrity": "sha512-yALhelO3i0hqZwhjtcr6dYyaLoCHbAMshwtj6cGxTvHZAKXHsYGdff6E8EPw3xLKY0ELUTQ69Q1rQiJENnccMA==", "dev": true }, + "@types/mime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.1.tgz", + "integrity": "sha512-FwI9gX75FgVBJ7ywgnq/P7tw+/o1GUbtP0KzbtusLigAOgIgNISRK0ZPl4qertvXSIE8YbsVJueQ90cDt9YYyw==", + "dev": true + }, "@types/node": { "version": "12.12.6", "resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.6.tgz", @@ -1213,6 +1268,12 @@ "integrity": "sha512-ce5d3q03Ex0sy4R14722Rmt6MT07Ua+k4FwDfdcToYJcMKNtRVQvJ6JCAPdAmAnbRb6CsX6aYb9m96NGod9uTw==", "dev": true }, + "@types/range-parser": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz", + "integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==", + "dev": true + }, "@types/resolve": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-0.0.8.tgz", @@ -1222,6 +1283,16 @@ "@types/node": "*" } }, + "@types/serve-static": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.3.tgz", + "integrity": "sha512-oprSwp094zOglVrXdlo/4bAHtKTAxX6VT8FOZlBKrmyLbNvE1zxZyJ6yikMVtHIvwP45+ZQGJn+FdXGKTozq0g==", + "dev": true, + "requires": { + "@types/express-serve-static-core": "*", + "@types/mime": "*" + } + }, "@types/stack-utils": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz", @@ -1401,6 +1472,12 @@ "integrity": "sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM=", "dev": true }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=", + "dev": true + }, "array-unique": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", @@ -1675,6 +1752,51 @@ "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", "dev": true }, + "body-parser": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", + "dev": true, + "requires": { + "bytes": "3.1.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "on-finished": "~2.3.0", + "qs": "6.7.0", + "raw-body": "2.4.0", + "type-is": "~1.6.17" + }, + "dependencies": { + "http-errors": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "dev": true, + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==", + "dev": true + } + } + }, "boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", @@ -1793,6 +1915,12 @@ "integrity": "sha512-k0KL0aWZuBt2lrxrcASWDfwOLMnodeQjodT/1SxEQAXsHANgo6ZC/VEaSEHCXt7aSTZ4/4H5LKa+tBXmW7Vtvw==", "dev": true }, + "bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==", + "dev": true + }, "cache-base": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", @@ -2076,6 +2204,21 @@ "utils-merge": "1.0.1" } }, + "content-disposition": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", + "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", + "dev": true, + "requires": { + "safe-buffer": "5.1.2" + } + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "dev": true + }, "convert-source-map": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", @@ -2085,6 +2228,18 @@ "safe-buffer": "~5.1.1" } }, + "cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==", + "dev": true + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=", + "dev": true + }, "copy-descriptor": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", @@ -2903,6 +3058,52 @@ "jest-regex-util": "^24.9.0" } }, + "express": { + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", + "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", + "dev": true, + "requires": { + "accepts": "~1.3.7", + "array-flatten": "1.1.1", + "body-parser": "1.19.0", + "content-disposition": "0.5.3", + "content-type": "~1.0.4", + "cookie": "0.4.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.1.2", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.5", + "qs": "6.7.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.1.2", + "send": "0.17.1", + "serve-static": "1.14.1", + "setprototypeof": "1.1.1", + "statuses": "~1.5.0", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "dependencies": { + "qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==", + "dev": true + } + } + }, "extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -3129,6 +3330,12 @@ "mime-types": "^2.1.12" } }, + "forwarded": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", + "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=", + "dev": true + }, "fragment-cache": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", @@ -3736,6 +3943,15 @@ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true }, + "get-port": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-5.0.0.tgz", + "integrity": "sha512-imzMU0FjsZqNa6BqOjbbW6w5BivHIuQKopjpPqcnx0AVHJQKCxK1O+Ab3OrVXhrekqfVMjwA9ZYu062R+KcIsQ==", + "dev": true, + "requires": { + "type-fest": "^0.3.0" + } + }, "get-stream": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", @@ -3844,9 +4060,9 @@ } }, "handlebars": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.5.1.tgz", - "integrity": "sha512-C29UoFzHe9yM61lOsIlCE5/mQVGrnIOrOq7maQl76L7tYPCgC1og0Ajt6uWnX4ZTxBPnjw+CUvawphwCfJgUnA==", + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.5.3.tgz", + "integrity": "sha512-3yPecJoJHK/4c6aZhSvxOyG4vJKDshV36VHp0iVCDVh7o9w2vwi3NSnL2MMPj3YdduqaBcu7cGbggJQM0br9xA==", "dev": true, "requires": { "neo-async": "^2.6.0", @@ -4112,6 +4328,12 @@ "loose-envify": "^1.0.0" } }, + "ipaddr.js": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.0.tgz", + "integrity": "sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA==", + "dev": true + }, "is-absolute-url": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-2.1.0.tgz", @@ -5102,6 +5324,14 @@ "proxy-middleware": "^0.15.0", "send": "^0.17.1", "serve-index": "^1.9.1" + }, + "dependencies": { + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "dev": true + } } }, "load-json-file": { @@ -5332,18 +5562,36 @@ "integrity": "sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==", "dev": true }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", + "dev": true + }, "memorystream": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", "integrity": "sha1-htcJCzDORV1j+64S3aUaR93K+bI=", "dev": true }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=", + "dev": true + }, "merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", "dev": true }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", + "dev": true + }, "microbundle": { "version": "0.12.0-next.6", "resolved": "https://registry.npmjs.org/microbundle/-/microbundle-0.12.0-next.6.tgz", @@ -5974,6 +6222,12 @@ "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", "dev": true }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=", + "dev": true + }, "path-type": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", @@ -6717,6 +6971,16 @@ "sisteransi": "^1.0.3" } }, + "proxy-addr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.5.tgz", + "integrity": "sha512-t/7RxHXPH6cJtP0pRG6smSr9QJidhB+3kXu0KgXnbGYMgzEnUxRQ4/LDdfOwZEMyIh3/xHb8PX3t+lfL9z+YVQ==", + "dev": true, + "requires": { + "forwarded": "~0.1.2", + "ipaddr.js": "1.9.0" + } + }, "proxy-middleware": { "version": "0.15.0", "resolved": "https://registry.npmjs.org/proxy-middleware/-/proxy-middleware-0.15.0.tgz", @@ -6763,6 +7027,39 @@ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", "dev": true }, + "raw-body": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", + "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", + "dev": true, + "requires": { + "bytes": "3.1.0", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "dependencies": { + "http-errors": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "dev": true, + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + } + } + }, "react-is": { "version": "16.11.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.11.0.tgz", @@ -7431,6 +7728,18 @@ } } }, + "serve-static": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", + "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", + "dev": true, + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.17.1" + } + }, "set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", @@ -8205,6 +8514,22 @@ "prelude-ls": "~1.1.2" } }, + "type-fest": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.3.1.tgz", + "integrity": "sha512-cUGJnCdr4STbePCgqNFbpVNCepa+kAVohJs1sLhxzdH+gnEoOd8VhbYa7pD3zZYGiURWM2xzEII3fQcRizDkYQ==", + "dev": true + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, "typescript": { "version": "3.7.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.7.2.tgz", @@ -8212,9 +8537,9 @@ "dev": true }, "uglify-js": { - "version": "3.6.8", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.6.8.tgz", - "integrity": "sha512-XhHJ3S3ZyMwP8kY1Gkugqx3CJh2C3O0y8NPiSxtm1tyD/pktLAkFZsFGpuNfTZddKDQ/bbDBLAd2YyA1pbi8HQ==", + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.7.0.tgz", + "integrity": "sha512-PC/ee458NEMITe1OufAjal65i6lB58R1HWMRcxwvdz1UopW0DYqlRL3xdu3IcTvTXsB02CRHykidkTRL+A3hQA==", "dev": true, "optional": true, "requires": { @@ -8569,6 +8894,11 @@ "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==", "dev": true }, + "xmlhttprequest": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/xmlhttprequest/-/xmlhttprequest-1.8.0.tgz", + "integrity": "sha1-Z/4HXFwk/vOfnWX197f+dRcZaPw=" + }, "y18n": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", diff --git a/package.json b/package.json index b01d8a9..f5fdb92 100644 --- a/package.json +++ b/package.json @@ -13,12 +13,21 @@ "dev": "run-p dev:*", "dev:test": "jest --watchAll", "dev:build": "microbundle watch -f modern,umd", + "storageserver": "ts-node src/server.ts", "prepare": "npm run build", "prepublishOnly": "npm test", "dev:serve": "live-server --host=localhost --port=5000 --no-browser --ignorePattern='.*\\.d\\.ts' dist" }, + "dependencies": { + "xmlhttprequest": "^1.8.0" + }, "devDependencies": { + "@types/cors": "^2.8.6", + "@types/express": "^4.17.2", "@types/uuid": "^3.4.6", + "cors": "^2.8.5", + "express": "^4.17.1", + "get-port": "^5.0.0", "tk-base": "^0.2.5", "uuid": "^3.3.3" }, diff --git a/src/index.test.ts b/src/index.test.ts index 34ab0f5..073348f 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -1,8 +1,9 @@ -import { getLocalStorage, getMemoryStorage } from "."; +import { getLocalStorage, getMemoryStorage, getRemoteStorage } from "."; import { MemoryStorage, ScopedStorage } from "./basic"; import { BrowserLocalStorage } from "./browser"; import { NodeDirectoryStorage } from "./node"; import { forceNodeStoragesInTempDir } from "./node.test"; +import { startTestServer } from "./remote.test"; const localStorage = (window as any).localStorage; @@ -42,6 +43,27 @@ describe(getLocalStorage, () => { }); }); +describe(getRemoteStorage, () => { + const server = startTestServer(); + + it("returns a scoped storage", async () => { + let storage = getRemoteStorage("app1", `http://localhost:${server.port()}`); + + await storage.set("testkey", "testvalue"); + expect(await storage.get("testkey")).toBe("testvalue"); + + storage = getRemoteStorage("app1", `http://localhost:${server.port()}`); + expect(await storage.get("testkey")).toBe("testvalue"); + + storage = getRemoteStorage("app2", `http://localhost:${server.port()}`); + expect(await storage.get("testkey")).toBeNull(); + }); + + it("scopes in a private namespace on shared mode", async () => { + + }); +}); + describe(getMemoryStorage, () => { it("returns a memory storage", () => { const storage = getMemoryStorage(); diff --git a/src/index.ts b/src/index.ts index 6cf2e40..2da0a2b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,12 +1,18 @@ -import { KeyValueStorage, MemoryStorage, ScopedStorage } from "./basic"; +import { KeyValueStorage, MemoryStorage, RefScopedStorage, ScopedStorage } from "./basic"; import { BrowserLocalStorage } from "./browser"; import { NodeDirectoryStorage } from "./node"; +import { RestRemoteStorage } from "./remote"; /** * Base type for storage usage */ export type TKStorage = KeyValueStorage +/** + * Exposed classes + */ +export { MemoryStorage, ScopedStorage, RefScopedStorage, BrowserLocalStorage, NodeDirectoryStorage, RestRemoteStorage }; + /** * Get the best "local" storage available */ @@ -23,6 +29,20 @@ export function getLocalStorage(appname: string): TKStorage { } } +/** + * Get a remote storage, based on an URI + * + * If *shared* is set to true, a namespace will be used to avoid collisions, using getLocalStorage to persist it + */ +export function getRemoteStorage(appname: string, uri: string, options = { shared: false }): TKStorage { + let storage: TKStorage = new RestRemoteStorage(appname, uri); + storage = new ScopedStorage(storage, appname); + if (options.shared) { + storage = new RefScopedStorage(getLocalStorage(appname), storage); + } + return storage; +} + /** * Get a in-memory volatile storage */ diff --git a/src/remote.test.ts b/src/remote.test.ts new file mode 100644 index 0000000..02beeef --- /dev/null +++ b/src/remote.test.ts @@ -0,0 +1,33 @@ +import getPort from "get-port"; +import { MemoryStorage } from "./basic"; +import { basicCheck } from "./basic.test"; +import { RestRemoteStorage } from "./remote"; +import { startRestServer } from "./server"; + +export function startTestServer(): { port: () => number } { + let app: { close: Function } | undefined; + let port: number; + + beforeEach(async () => { + port = await getPort(); + app = await startRestServer(port, new MemoryStorage()); + }); + + afterEach(async () => { + if (app) { + await app.close(); + app = undefined; + } + }); + + return { port: () => port }; +} + +describe(RestRemoteStorage, () => { + const server = startTestServer(); + + it("communicates with the server", async () => { + const storage = new RestRemoteStorage("test", `http://localhost:${server.port()}`); + await basicCheck(storage); + }); +}); diff --git a/src/remote.ts b/src/remote.ts new file mode 100644 index 0000000..e9c6037 --- /dev/null +++ b/src/remote.ts @@ -0,0 +1,64 @@ +import { KeyValueStorage } from "./basic"; + +function protectKey(key: string): string { + return encodeURIComponent(key); +} + +type RestClient = (method: "GET" | "POST" | "PUT" | "DELETE", uri: string, body?: string) => Promise +export const HEADER_REQUESTER = "X-TK-Storage-App" +export const HEADER_REPLYIER = "X-TK-Storage-Control" + +/** + * Storage on a remote server, directly usable using HTTP REST verbs + */ +export class RestRemoteStorage implements KeyValueStorage { + readonly appname: string + readonly base_url: string + readonly client: RestClient + + constructor(appname: string, url: string) { + this.appname = appname; + this.base_url = (url[url.length - 1] != "/") ? url + "/" : url; + this.client = this.createClient(); + } + + async get(key: string): Promise { + key = protectKey(key); + return await this.client("GET", key); + } + + async set(key: string, value: string | null): Promise { + key = protectKey(key); + if (value === null) { + await this.client("DELETE", key); + } else { + await this.client("PUT", key, value); + } + } + + private createClient(): RestClient { + const Request: typeof XMLHttpRequest = (typeof XMLHttpRequest === "undefined") ? require("xmlhttprequest").XMLHttpRequest : XMLHttpRequest; + const client: RestClient = (method, path, body?) => new Promise((resolve, reject) => { + const xhr = new Request(); + xhr.open(method, `${this.base_url}${path}`); + xhr.responseType = 'text'; + xhr.setRequestHeader(HEADER_REQUESTER, this.appname); + xhr.onload = function () { + if (xhr.getResponseHeader(HEADER_REPLYIER) != "ok") { + reject("storage not compatible with tk-storage"); + } else if (xhr.status == 200) { + resolve(xhr.responseText); + } else if (xhr.status == 404) { + resolve(null); + } else { + reject(`http error ${xhr.status}: ${xhr.statusText}`); + } + }; + xhr.onerror = function () { + reject("unknown error"); + }; + xhr.send(body); + }); + return client; + } +} diff --git a/src/server.ts b/src/server.ts new file mode 100644 index 0000000..7d137bf --- /dev/null +++ b/src/server.ts @@ -0,0 +1,64 @@ +import bodyParser from "body-parser"; +import cors from "cors"; +import express from "express"; +import { getLocalStorage } from "."; +import { KeyValueStorage } from "./basic"; +import { HEADER_REPLYIER, HEADER_REQUESTER } from "./remote"; + +type Server = { + close: () => Promise +} + +/** + * Start a server compliant with RestRemoteStorage + */ +export function startRestServer(port: number, storage: KeyValueStorage): Promise { + const app = express(); + app.use( + cors({ exposedHeaders: [HEADER_REPLYIER] }), + bodyParser.text(), + (req, res, next) => { + if (req.header(HEADER_REQUESTER)) { + res.set(HEADER_REPLYIER, "ok"); + next(); + } else { + res.status(400).send(); + } + }, + ); + app.get("*", (req, res, next) => { + storage.get(req.path).then(result => { + if (result === null) { + res.status(404).send(); + } else { + res.send(result); + } + }).catch(next); + }); + app.put("*", (req, res, next) => { + storage.set(req.path, req.body).then(() => { + res.send(); + }).catch(next); + }); + app.delete("*", (req, res, next) => { + storage.set(req.path, null).then(() => { + res.send(); + }).catch(next); + }); + + return new Promise((resolve) => { + const server = app.listen(port, () => { + resolve({ + close: () => new Promise((resolve, reject) => { + server.close((err) => err ? reject(err) : resolve()); + }) + }) + }); + }); +} + +if (typeof require !== "undefined" && require.main === module) { + startRestServer(5001, getLocalStorage("tk-storage-server")).then(() => { + console.log(`Server running on port 5001`); + }); +}