From af87dfa339ad19393d23df207e5554580e9f96d4 Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Thu, 26 Nov 2020 21:52:05 +0100 Subject: [PATCH 001/120] initial Typescript config --- package-lock.json | 164 ++++++++++++++++++++++++++++++++++++++++++++++ package.json | 6 ++ tsconfig.json | 49 ++++++++++++++ webpack.config.js | 23 +++++-- 4 files changed, 235 insertions(+), 7 deletions(-) create mode 100644 tsconfig.json diff --git a/package-lock.json b/package-lock.json index f0e87f0f..27a8e630 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3853,6 +3853,15 @@ "integrity": "sha512-46+j5QxbPWza0PB1i15nZx0xQ4I/EfQxg9J8Had3b408SV63nEtor2e+oiY63amTo9KTuh2a3XLObNwduxYwwA==", "dev": true }, + "errno": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz", + "integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==", + "dev": true, + "requires": { + "prr": "~1.0.1" + } + }, "error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -8023,6 +8032,12 @@ "integrity": "sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=", "dev": true }, + "prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=", + "dev": true + }, "pseudomap": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", @@ -8952,6 +8967,51 @@ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true }, + "source-map-loader": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-1.1.2.tgz", + "integrity": "sha512-bjf6eSENOYBX4JZDfl9vVLNsGAQ6Uz90fLmOazcmMcyDYOBFsGxPNn83jXezWLY9bJsVAo1ObztxPcV8HAbjVA==", + "dev": true, + "requires": { + "abab": "^2.0.5", + "iconv-lite": "^0.6.2", + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0", + "source-map": "^0.6.1", + "whatwg-mimetype": "^2.3.0" + }, + "dependencies": { + "iconv-lite": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.2.tgz", + "integrity": "sha512-2y91h5OpQlolefMPmUlivelittSWy0rP+oYVpn6A7GwVHNE8AWzoYOBNmlwks3LobaJxgHCYZAnyNo2GgpNRNQ==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + } + }, + "json5": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz", + "integrity": "sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, + "loader-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz", + "integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + } + } + } + }, "source-map-resolve": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", @@ -9548,6 +9608,104 @@ "punycode": "^2.1.1" } }, + "ts-loader": { + "version": "8.0.11", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-8.0.11.tgz", + "integrity": "sha512-06X+mWA2JXoXJHYAesUUL4mHFYhnmyoCdQVMXofXF552Lzd4wNwSGg7unJpttqUP7ziaruM8d7u8LUB6I1sgzA==", + "dev": true, + "requires": { + "chalk": "^2.3.0", + "enhanced-resolve": "^4.0.0", + "loader-utils": "^1.0.2", + "micromatch": "^4.0.0", + "semver": "^6.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "enhanced-resolve": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.3.0.tgz", + "integrity": "sha512-3e87LvavsdxyoCfGusJnrZ5G8SLPOFeHSNpZI/ATL9a5leXo2k0w6MKnbqhdBad9qTobSfB20Ld7UmgoNbAZkQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "memory-fs": "^0.5.0", + "tapable": "^1.0.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "memory-fs": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.5.0.tgz", + "integrity": "sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA==", + "dev": true, + "requires": { + "errno": "^0.1.3", + "readable-stream": "^2.0.1" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "tapable": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", + "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", + "dev": true + } + } + }, "tsconfig-paths": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.9.0.tgz", @@ -9625,6 +9783,12 @@ "is-typedarray": "^1.0.0" } }, + "typescript": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.2.tgz", + "integrity": "sha512-thGloWsGH3SOxv1SoY7QojKi0tc+8FnOmiarEGMbd/lar7QOEd3hvlx3Fp5y6FlDUGl9L+pd4n2e+oToGMmhRQ==", + "dev": true + }, "typical": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/typical/-/typical-5.2.0.tgz", diff --git a/package.json b/package.json index 728c75dc..0053eff1 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,9 @@ "lint": "cross-env eslint src --ext .js", "test": "cross-env npm run build && jest" }, + "engines": { + "node": ">=0.12" + }, "repository": { "type": "git", "url": "git+https://github.com/ecamp/hal-json-vuex.git" @@ -53,6 +56,9 @@ "jest": "^26.6.3", "lodash": "^4.17.20", "rimraf": "^3.0.2", + "source-map-loader": "^1.1.2", + "ts-loader": "^8.0.11", + "typescript": "^4.1.2", "vue": "^2.6.12", "vue-axios": "^2.1.5", "vue-template-compiler": "^2.6.12", diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..92690e5d --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,49 @@ +{ + "compilerOptions": { + "target": "es2018", + "module": "esnext", + "strict": true, + "importHelpers": true, + "moduleResolution": "node", + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "baseUrl": ".", + "rootDir": ".", + "outDir": "dist", + "sourceMap": true, + "declaration": true, + "declarationMap": true, + "removeComments": false, + "noImplicitAny": true, + "noImplicitReturns": true, + "noImplicitThis": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "strictNullChecks": true, + "suppressImplicitAnyIndexErrors": true, + "lib": [ + "esnext", + "dom", + "es2017" + ], + "paths": { + "@/*": [ + "src/*" + ], + "test/*": [ + "tests/*" + ] + }, + "types": [ + "node", + "jest" + ] + }, + "include": [ + "src", + "tests" + ], + "exclude": [ + "node_modules" + ] +} \ No newline at end of file diff --git a/webpack.config.js b/webpack.config.js index 5a72fd03..a6a3865b 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,12 +1,12 @@ -var path = require('path'); -var webpack = require('webpack'); -var nodeExternals = require('webpack-node-externals'); +var path = require('path') +var webpack = require('webpack') +var nodeExternals = require('webpack-node-externals') module.exports = { devtool: 'source-map', externals: [nodeExternals()], entry: [ - './src/index' + './src/index.js' ], output: { path: path.join(__dirname, 'dist'), @@ -16,19 +16,28 @@ module.exports = { plugins: [ new webpack.DefinePlugin({ 'process.env': { - 'NODE_ENV': JSON.stringify('production') + NODE_ENV: JSON.stringify('production') } }) ], module: { rules: [ - { test: /\.js?$/, use: ['babel-loader', 'eslint-loader'], exclude: /node_modules/ }, + // All files with a '.ts' or '.tsx' extension will be handled by 'ts-loader'. + { test: /\.tsx?$/, loader: 'ts-loader' }, + + // All output '.js' files will have any sourcemaps re-processed by 'source-map-loader'. + { test: /\.js$/, loader: 'source-map-loader' } + + // { test: /\.js?$/, use: ['babel-loader', 'eslint-loader'], exclude: /node_modules/ }, ] }, resolve: { + // Add '.ts' and '.tsx' as resolvable extensions. + // extensions: ['', '.webpack.js', '.web.js', '.ts', '.tsx', '.js'], + modules: [ path.join(__dirname, 'src'), 'node_modules' ] } -}; +} From f914c8633074b6ceefedc8290bfe07322aac089a Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Thu, 26 Nov 2020 23:00:22 +0100 Subject: [PATCH 002/120] extract ServerException & fix eslint & fix tests --- .babelrc | 5 +- .eslintrc | 6 +- jest.config.js | 8 + package-lock.json | 390 ++++++++++++++++++++++++++++++++++++----- package.json | 9 +- src/ServerException.ts | 22 +++ src/index.js | 18 +- webpack.config.js | 4 +- 8 files changed, 393 insertions(+), 69 deletions(-) create mode 100644 jest.config.js create mode 100644 src/ServerException.ts diff --git a/.babelrc b/.babelrc index b82d80e8..e00fda5c 100644 --- a/.babelrc +++ b/.babelrc @@ -1,10 +1,11 @@ { "presets": [ - "@babel/preset-env" + "@babel/preset-env", + "@babel/preset-typescript" ], "plugins": [ "@babel/plugin-transform-regenerator", "@babel/plugin-transform-runtime" ], "sourceType": "module" -} +} \ No newline at end of file diff --git a/.eslintrc b/.eslintrc index 1c8674ae..b8b74389 100644 --- a/.eslintrc +++ b/.eslintrc @@ -4,6 +4,10 @@ "node": true, "jest": true }, + "parser": "@typescript-eslint/parser", + "plugins": [ + "@typescript-eslint" + ], "extends": [ "plugin:vue/recommended", "@vue/standard" @@ -56,4 +60,4 @@ "parserOptions": { "parser": "babel-eslint" } -} +} \ No newline at end of file diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 00000000..b6e5a23f --- /dev/null +++ b/jest.config.js @@ -0,0 +1,8 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'jsdom', + transform: { + '^.+\\.ts?$': 'ts-jest', + '^.+\\.js?$': 'babel-jest' + } +} diff --git a/package-lock.json b/package-lock.json index 27a8e630..a4dec24f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -725,6 +725,15 @@ "@babel/helper-plugin-utils": "^7.10.4" } }, + "@babel/plugin-syntax-typescript": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.12.1.tgz", + "integrity": "sha512-UZNEcCY+4Dp9yYRCAHrHDU+9ZXLYaY9MgBXSRLkB9WjYFRR6quJBumfVrEkUxrePPBwFcpWfNKXqVRQQtm7mMA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, "@babel/plugin-transform-arrow-functions": { "version": "7.12.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.12.1.tgz", @@ -1040,6 +1049,17 @@ "@babel/helper-plugin-utils": "^7.10.4" } }, + "@babel/plugin-transform-typescript": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.12.1.tgz", + "integrity": "sha512-VrsBByqAIntM+EYMqSm59SiMEf7qkmI9dqMt6RbD/wlwueWmYcI0FFK5Fj47pP6DRZm+3teXjosKlwcZJ5lIMw==", + "dev": true, + "requires": { + "@babel/helper-create-class-features-plugin": "^7.12.1", + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-syntax-typescript": "^7.12.1" + } + }, "@babel/plugin-transform-unicode-escapes": { "version": "7.12.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.12.1.tgz", @@ -1146,6 +1166,17 @@ "esutils": "^2.0.2" } }, + "@babel/preset-typescript": { + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.12.7.tgz", + "integrity": "sha512-nOoIqIqBmHBSEgBXWR4Dv/XBehtIFcw9PqZw6rFYuKrzsZmOQm3PR5siLBnKZFEsDb03IegG8nSjU/iXXXYRmw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-validator-option": "^7.12.1", + "@babel/plugin-transform-typescript": "^7.12.1" + } + }, "@babel/runtime": { "version": "7.12.5", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.12.5.tgz", @@ -1251,6 +1282,14 @@ "lodash": "^4.17.19", "minimatch": "^3.0.4", "strip-json-comments": "^3.1.1" + }, + "dependencies": { + "ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true + } } }, "@istanbuljs/load-nyc-config": { @@ -1670,6 +1709,32 @@ } } }, + "@nodelib/fs.scandir": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz", + "integrity": "sha512-eGmwYQn3gxo4r7jdQnkrrN6bY478C3P+a/y72IJukF8LjB6ZHeB3c+Ehacj3sYeSmUXGlnA67/PmbM9CVwL7Dw==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "2.0.3", + "run-parallel": "^1.1.9" + } + }, + "@nodelib/fs.stat": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.3.tgz", + "integrity": "sha512-bQBFruR2TAwoevBEd/NWMoAAtNGzTRgdrqnYCc7dhzfoNvqPzLyqlEQnzZ3kVnNrSp25iyxE00/3h2fqGAGArA==", + "dev": true + }, + "@nodelib/fs.walk": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.4.tgz", + "integrity": "sha512-1V9XOY4rDW0rehzbrcqAmHnz8e7SKvX27gh8Gt2WgB0+pdzdiLV83p72kZPU+jvMbS1qU5mauP2iOvO8rhmurQ==", + "dev": true, + "requires": { + "@nodelib/fs.scandir": "2.1.3", + "fastq": "^1.6.0" + } + }, "@sinonjs/commons": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.1.tgz", @@ -1711,9 +1776,9 @@ } }, "@types/babel__template": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.0.3.tgz", - "integrity": "sha512-uCoznIPDmnickEi6D0v11SBpW0OuVqHJCa7syXqQHy5uktSCreIlt0iglsCnmvz8yCb38hGcWeseA8cWJSwv5Q==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.0.tgz", + "integrity": "sha512-NTPErx4/FiPCGScH7foPyr+/1Dkzkni+rHiYHHoTjvwou7AQzJkNeD60A9CXRy+ZEN2B1bggmkTMCDb+Mv5k+A==", "dev": true, "requires": { "@babel/parser": "^7.1.0", @@ -1788,6 +1853,16 @@ "@types/istanbul-lib-report": "*" } }, + "@types/jest": { + "version": "26.0.15", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-26.0.15.tgz", + "integrity": "sha512-s2VMReFXRg9XXxV+CW9e5Nz8fH2K1aEhwgjUqPPbQd7g95T0laAcvLv032EhFHIa5GHsZ8W7iJEQVaJq6k3Gog==", + "dev": true, + "requires": { + "jest-diff": "^26.0.0", + "pretty-format": "^26.0.0" + } + }, "@types/json-schema": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.6.tgz", @@ -1839,6 +1914,105 @@ "integrity": "sha512-FA/BWv8t8ZWJ+gEOnLLd8ygxH/2UFbAvgEonyfN6yWGLKc7zVjbpl2Y4CTjid9h2RfgPP6SEt6uHwEOply00yw==", "dev": true }, + "@typescript-eslint/eslint-plugin": { + "version": "4.8.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.8.2.tgz", + "integrity": "sha512-gQ06QLV5l1DtvYtqOyFLXD9PdcILYqlrJj2l+CGDlPtmgLUzc1GpqciJFIRvyfvgLALpnxYINFuw+n9AZhPBKQ==", + "dev": true, + "requires": { + "@typescript-eslint/experimental-utils": "4.8.2", + "@typescript-eslint/scope-manager": "4.8.2", + "debug": "^4.1.1", + "functional-red-black-tree": "^1.0.1", + "regexpp": "^3.0.0", + "semver": "^7.3.2", + "tsutils": "^3.17.1" + }, + "dependencies": { + "semver": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", + "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==", + "dev": true + } + } + }, + "@typescript-eslint/experimental-utils": { + "version": "4.8.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.8.2.tgz", + "integrity": "sha512-hpTw6o6IhBZEsQsjuw/4RWmceRyESfAiEzAEnXHKG1X7S5DXFaZ4IO1JO7CW1aQ604leQBzjZmuMI9QBCAJX8Q==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.3", + "@typescript-eslint/scope-manager": "4.8.2", + "@typescript-eslint/types": "4.8.2", + "@typescript-eslint/typescript-estree": "4.8.2", + "eslint-scope": "^5.0.0", + "eslint-utils": "^2.0.0" + } + }, + "@typescript-eslint/parser": { + "version": "4.8.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.8.2.tgz", + "integrity": "sha512-u0leyJqmclYr3KcXOqd2fmx6SDGBO0MUNHHAjr0JS4Crbb3C3d8dwAdlazy133PLCcPn+aOUFiHn72wcuc5wYw==", + "dev": true, + "requires": { + "@typescript-eslint/scope-manager": "4.8.2", + "@typescript-eslint/types": "4.8.2", + "@typescript-eslint/typescript-estree": "4.8.2", + "debug": "^4.1.1" + } + }, + "@typescript-eslint/scope-manager": { + "version": "4.8.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.8.2.tgz", + "integrity": "sha512-qHQ8ODi7mMin4Sq2eh/6eu03uVzsf5TX+J43xRmiq8ujng7ViQSHNPLOHGw/Wr5dFEoxq/ubKhzClIIdQy5q3g==", + "dev": true, + "requires": { + "@typescript-eslint/types": "4.8.2", + "@typescript-eslint/visitor-keys": "4.8.2" + } + }, + "@typescript-eslint/types": { + "version": "4.8.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.8.2.tgz", + "integrity": "sha512-z1/AVcVF8ju5ObaHe2fOpZYEQrwHyZ7PTOlmjd3EoFeX9sv7UekQhfrCmgUO7PruLNfSHrJGQvrW3Q7xQ8EoAw==", + "dev": true + }, + "@typescript-eslint/typescript-estree": { + "version": "4.8.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.8.2.tgz", + "integrity": "sha512-HToGNwI6fekH0dOw3XEVESUm71Onfam0AKin6f26S2FtUmO7o3cLlWgrIaT1q3vjB3wCTdww3Dx2iGq5wtUOCg==", + "dev": true, + "requires": { + "@typescript-eslint/types": "4.8.2", + "@typescript-eslint/visitor-keys": "4.8.2", + "debug": "^4.1.1", + "globby": "^11.0.1", + "is-glob": "^4.0.1", + "lodash": "^4.17.15", + "semver": "^7.3.2", + "tsutils": "^3.17.1" + }, + "dependencies": { + "semver": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", + "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==", + "dev": true + } + } + }, + "@typescript-eslint/visitor-keys": { + "version": "4.8.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.8.2.tgz", + "integrity": "sha512-Vg+/SJTMZJEKKGHW7YC21QxgKJrSbxoYYd3MEUGtW7zuytHuEcksewq0DUmo4eh/CTNrVJGSdIY9AtRb6riWFw==", + "dev": true, + "requires": { + "@typescript-eslint/types": "4.8.2", + "eslint-visitor-keys": "^2.0.0" + } + }, "@vue/eslint-config-standard": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/@vue/eslint-config-standard/-/eslint-config-standard-5.1.2.tgz", @@ -2264,6 +2438,12 @@ } } }, + "array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true + }, "array-unique": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", @@ -2462,42 +2642,6 @@ "chalk": "^4.0.0", "graceful-fs": "^4.2.4", "slash": "^3.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", - "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } } }, "babel-loader": { @@ -2908,6 +3052,15 @@ "node-releases": "^1.1.66" } }, + "bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "requires": { + "fast-json-stable-stringify": "2.x" + } + }, "bser": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", @@ -3693,6 +3846,23 @@ } } }, + "dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "requires": { + "path-type": "^4.0.0" + }, + "dependencies": { + "path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true + } + } + }, "doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -3942,9 +4112,9 @@ } }, "eslint": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.13.0.tgz", - "integrity": "sha512-uCORMuOO8tUzJmsdRtrvcGq5qposf7Rw0LwkTJkoDbOycVQtQjmnhZSuLQnozLE4TmAzlMVV45eCHmQ1OpDKUQ==", + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.14.0.tgz", + "integrity": "sha512-5YubdnPXrlrYAFCKybPuHIAH++PINe1pmKNc5wQRB9HSbqIK1ywAnntE3Wwua4giKu0bjligf1gLF6qxMGOYRA==", "dev": true, "requires": { "@babel/code-frame": "^7.0.0", @@ -3997,6 +4167,12 @@ "which": "^2.0.1" } }, + "ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true + }, "levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -4830,6 +5006,20 @@ "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==", "dev": true }, + "fast-glob": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.4.tgz", + "integrity": "sha512-kr/Oo6PX51265qeuCYsyGypiO5uJFgBS0jksyG7FUeCyQzNwYnzrNIMR1NXfkZXsMYXYLRAHgISHBz8gQcxKHQ==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.0", + "merge2": "^1.3.0", + "micromatch": "^4.0.2", + "picomatch": "^2.2.1" + } + }, "fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -4842,6 +5032,15 @@ "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", "dev": true }, + "fastq": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.9.0.tgz", + "integrity": "sha512-i7FVWL8HhVY+CTkwFxkN2mk3h+787ixS5S63eb78diVRc1MCssarHq3W5cj0av7YDSwmaV928RNag+U1etRQ7w==", + "dev": true, + "requires": { + "reusify": "^1.0.4" + } + }, "fb-watchman": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.1.tgz", @@ -5097,6 +5296,20 @@ "type-fest": "^0.8.1" } }, + "globby": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.1.tgz", + "integrity": "sha512-iH9RmgwCmUJHi2z5o2l3eTtGBtXek1OYlHrbcxOYugyHLmAsZrPj43OtHThd62Buh/Vv6VyCBD2bdyWcGNQqoQ==", + "dev": true, + "requires": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.1.1", + "ignore": "^5.1.4", + "merge2": "^1.3.0", + "slash": "^3.0.0" + } + }, "graceful-fs": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", @@ -5342,9 +5555,9 @@ "dev": true }, "ignore": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", - "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "version": "5.1.8", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz", + "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==", "dev": true }, "import-fresh": { @@ -7129,6 +7342,12 @@ "integrity": "sha1-Tpho1FJXXXUK/9NYyXlUPcIO1Xc=", "dev": true }, + "lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=", + "dev": true + }, "lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -7168,6 +7387,12 @@ } } }, + "make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, "makeerror": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.11.tgz", @@ -7230,6 +7455,12 @@ "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", "dev": true }, + "merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true + }, "micromatch": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", @@ -8484,6 +8715,12 @@ "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", "dev": true }, + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true + }, "rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -8509,6 +8746,12 @@ "integrity": "sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==", "dev": true }, + "run-parallel": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.1.10.tgz", + "integrity": "sha512-zb/1OuZ6flOlH6tQyMPUrE3x3Ulxjlo9WIVXR4yVYi4H9UXQaeIsPbLn2R3O3vQCnDKkAl2qHiuocKKX4Tz/Sw==", + "dev": true + }, "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", @@ -8798,6 +9041,12 @@ "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", "dev": true }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, "slice-ansi": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", @@ -9608,6 +9857,48 @@ "punycode": "^2.1.1" } }, + "ts-jest": { + "version": "26.4.4", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-26.4.4.tgz", + "integrity": "sha512-3lFWKbLxJm34QxyVNNCgXX1u4o/RV0myvA2y2Bxm46iGIjKlaY0own9gIckbjZJPn+WaJEnfPPJ20HHGpoq4yg==", + "dev": true, + "requires": { + "@types/jest": "26.x", + "bs-logger": "0.x", + "buffer-from": "1.x", + "fast-json-stable-stringify": "2.x", + "jest-util": "^26.1.0", + "json5": "2.x", + "lodash.memoize": "4.x", + "make-error": "1.x", + "mkdirp": "1.x", + "semver": "7.x", + "yargs-parser": "20.x" + }, + "dependencies": { + "json5": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz", + "integrity": "sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, + "semver": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", + "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==", + "dev": true + }, + "yargs-parser": { + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "dev": true + } + } + }, "ts-loader": { "version": "8.0.11", "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-8.0.11.tgz", @@ -9732,6 +10023,15 @@ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", "dev": true }, + "tsutils": { + "version": "3.17.1", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.17.1.tgz", + "integrity": "sha512-kzeQ5B8H3w60nFY2g8cJIuH7JDpsALXySGtwGJ0p2LSjLgay3NdIpqq5SoOBe46bKDW2iq25irHCr8wjomUS2g==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } + }, "tty-browserify": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", diff --git a/package.json b/package.json index 0053eff1..33c5a7b9 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "build": "cross-env NODE_ENV=production webpack", "clean": "cross-env rimraf dist coverage lib", "coverage": "cross-env npm run build && jest --coverage", - "lint": "cross-env eslint src --ext .js", + "lint": "cross-env eslint src --ext .js,.ts", "test": "cross-env npm run build && jest" }, "engines": { @@ -39,14 +39,18 @@ "@babel/plugin-transform-regenerator": "^7.12.1", "@babel/plugin-transform-runtime": "^7.12.1", "@babel/preset-env": "^7.12.1", + "@babel/preset-typescript": "^7.12.7", + "@typescript-eslint/eslint-plugin": "^4.8.2", + "@typescript-eslint/parser": "^4.8.2", "@vue/eslint-config-standard": "^5.1.2", "@vue/test-utils": "^1.1.1", "axios": "^0.21.0", "axios-mock-adapter": "^1.19.0", "babel-eslint": "^10.1.0", + "babel-jest": "^26.6.3", "babel-loader": "^8.2.0", "cross-env": "^7.0.2", - "eslint": "^7.13.0", + "eslint": "^7.14.0", "eslint-loader": "^4.0.2", "eslint-plugin-import": "^2.22.1", "eslint-plugin-node": "^11.1.0", @@ -57,6 +61,7 @@ "lodash": "^4.17.20", "rimraf": "^3.0.2", "source-map-loader": "^1.1.2", + "ts-jest": "^26.4.4", "ts-loader": "^8.0.11", "typescript": "^4.1.2", "vue": "^2.6.12", diff --git a/src/ServerException.ts b/src/ServerException.ts new file mode 100644 index 00000000..159f6d4e --- /dev/null +++ b/src/ServerException.ts @@ -0,0 +1,22 @@ +import { AxiosResponse } from 'axios' + +/** + * Error class for returning server exceptions (attaches Axios response object to error) + */ +export default class ServerException extends Error { + public response: AxiosResponse + + /** + * @param response Axios reponse object + * @param params Standard Error parameters + */ + public constructor (response: AxiosResponse, ...params: any[]) { + super(...params) + + if (!this.message) { + this.message = 'Server error ' + response.status + ' (' + response.statusText + ')' + } + this.name = 'ServerException' + this.response = response + } +} diff --git a/src/index.js b/src/index.js index 708a47fd..57ce358c 100644 --- a/src/index.js +++ b/src/index.js @@ -3,23 +3,7 @@ import urltemplate from 'url-template' import { normalizeEntityUri } from './normalizeUri' import StoreValueProxyCreator from './storeValueProxy' import storeModule from './storeModule' - -/** - * Error class for returning server exceptions (attaches response object to error) - * @param response Axios response object - * @param ...params Any other parameters from default Error constructor (message, etc.) - */ -export class ServerException extends Error { - constructor (response, ...params) { - super(...params) - - if (!this.message) { - this.message = 'Server error ' + response.status + ' (' + response.statusText + ')' - } - this.name = 'ServerException' - this.response = response - } -} +import ServerException from './ServerException.ts' /** * Defines the API store methods available in all Vue components. The methods can be called as follows: diff --git a/webpack.config.js b/webpack.config.js index a6a3865b..97232c87 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -26,9 +26,9 @@ module.exports = { { test: /\.tsx?$/, loader: 'ts-loader' }, // All output '.js' files will have any sourcemaps re-processed by 'source-map-loader'. - { test: /\.js$/, loader: 'source-map-loader' } + { test: /\.js$/, loader: 'source-map-loader' }, - // { test: /\.js?$/, use: ['babel-loader', 'eslint-loader'], exclude: /node_modules/ }, + { test: /\.js?$/, use: ['babel-loader', 'eslint-loader'], exclude: /node_modules/ } ] }, resolve: { From 3bc11334aa965cca4241cea5d90e5d4db8727418 Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Tue, 1 Dec 2020 21:51:20 +0100 Subject: [PATCH 003/120] Typescript conversion for StoreValue & QueryablePromise + necessary interface definitions --- .eslintrc | 24 +- package-lock.json | 555 ++++++++++++++++-- package.json | 10 +- ...ueryablePromise.js => QueryablePromise.ts} | 6 +- src/ServerException.ts | 4 +- src/StoreValue.js | 66 --- src/StoreValue.ts | 97 +++ src/StoreValueCreator.js | 2 +- src/index.js | 2 +- src/interfaces/ApiActions.ts | 12 + src/interfaces/Config.ts | 15 + src/interfaces/Resource.ts | 16 + src/interfaces/StoreData.ts | 10 + tsconfig.json | 17 +- webpack.config.js | 9 +- 15 files changed, 711 insertions(+), 134 deletions(-) rename src/{QueryablePromise.js => QueryablePromise.ts} (94%) delete mode 100644 src/StoreValue.js create mode 100644 src/StoreValue.ts create mode 100644 src/interfaces/ApiActions.ts create mode 100644 src/interfaces/Config.ts create mode 100644 src/interfaces/Resource.ts create mode 100644 src/interfaces/StoreData.ts diff --git a/.eslintrc b/.eslintrc index b8b74389..7d435ae4 100644 --- a/.eslintrc +++ b/.eslintrc @@ -4,14 +4,30 @@ "node": true, "jest": true }, - "parser": "@typescript-eslint/parser", - "plugins": [ - "@typescript-eslint" - ], "extends": [ + "eslint:recommended", "plugin:vue/recommended", "@vue/standard" ], + "overrides": [ + { + "files": [ + "**/*.ts", + "**/*.tsx" + ], + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended", + "plugin:vue/recommended", + "@vue/standard" + ], + "parser": "@typescript-eslint/parser", + "plugins": [ + "@typescript-eslint" + ], + } + ], "rules": { "vue/component-tags-order": [ "error", diff --git a/package-lock.json b/package-lock.json index a4dec24f..86ebec04 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1735,6 +1735,40 @@ "fastq": "^1.6.0" } }, + "@nuxt/types": { + "version": "2.14.7", + "resolved": "https://registry.npmjs.org/@nuxt/types/-/types-2.14.7.tgz", + "integrity": "sha512-7aLOQDCb4wYMGHYj0EVBieqMC4aOo7ZVFP5jjWxuWSjzOroRWVE3TB10+wcu9E7FoOPT99vd4+bJ+He/WpDpBQ==", + "dev": true, + "requires": { + "@types/autoprefixer": "^9.7.2", + "@types/babel__core": "^7.1.10", + "@types/compression": "^1.7.0", + "@types/connect": "^3.4.33", + "@types/etag": "^1.8.0", + "@types/file-loader": "^4.2.0", + "@types/html-minifier": "^4.0.0", + "@types/less": "^3.0.1", + "@types/node": "^12.12.67", + "@types/node-sass": "^4.11.1", + "@types/optimize-css-assets-webpack-plugin": "^5.0.1", + "@types/pug": "^2.0.4", + "@types/serve-static": "^1.13.5", + "@types/terser-webpack-plugin": "^2.2.0", + "@types/webpack": "^4.41.22", + "@types/webpack-bundle-analyzer": "^3.8.0", + "@types/webpack-dev-middleware": "^3.7.2", + "@types/webpack-hot-middleware": "^2.25.3" + }, + "dependencies": { + "@types/node": { + "version": "12.19.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.19.7.tgz", + "integrity": "sha512-zvjOU1g4CpPilbTDUATnZCUb/6lARMRAqzT7ILwl1P3YvU2leEcZ2+fw9+Jrw/paXB1CgQyXTrN4hWDtqT9O2A==", + "dev": true + } + } + }, "@sinonjs/commons": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.1.tgz", @@ -1753,6 +1787,22 @@ "@sinonjs/commons": "^1.7.0" } }, + "@types/anymatch": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@types/anymatch/-/anymatch-1.3.1.tgz", + "integrity": "sha512-/+CRPXpBDpo2RK9C68N3b2cOvO0Cf5B9aPijHsoDQTHivnGSObdOF2BRQOYjojWTDy6nQvMjmqRXIxH55VjxxA==", + "dev": true + }, + "@types/autoprefixer": { + "version": "9.7.2", + "resolved": "https://registry.npmjs.org/@types/autoprefixer/-/autoprefixer-9.7.2.tgz", + "integrity": "sha512-QX7U7YW3zX3ex6MECtWO9folTGsXeP4b8bSjTq3I1ODM+H+sFHwGKuof+T+qBcDClGlCGtDb3SVfiTVfmcxw4g==", + "dev": true, + "requires": { + "@types/browserslist": "*", + "postcss": "7.x.x" + } + }, "@types/babel__core": { "version": "7.1.12", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.12.tgz", @@ -1794,6 +1844,49 @@ "@babel/types": "^7.3.0" } }, + "@types/body-parser": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ==", + "dev": true, + "requires": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "@types/browserslist": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/@types/browserslist/-/browserslist-4.8.0.tgz", + "integrity": "sha512-4PyO9OM08APvxxo1NmQyQKlJdowPCOQIy5D/NLO3aO0vGC57wsMptvGp3b8IbYnupFZr92l1dlVief1JvS6STQ==", + "dev": true + }, + "@types/clean-css": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@types/clean-css/-/clean-css-4.2.2.tgz", + "integrity": "sha512-xiTJn3bmDh1lA8c6iVJs4ZhHw+pcmxXlJQXOB6G1oULaak8rmarIeFKI4aTJ7849dEhaO612wgIualZfbxTJwA==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/compression": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@types/compression/-/compression-1.7.0.tgz", + "integrity": "sha512-3LzWUM+3k3XdWOUk/RO+uSjv7YWOatYq2QADJntK1pjkk4DfVP0KrIEPDnXRJxAAGKe0VpIPRmlINLDuCedZWw==", + "dev": true, + "requires": { + "@types/express": "*" + } + }, + "@types/connect": { + "version": "3.4.33", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.33.tgz", + "integrity": "sha512-2+FrkXY4zllzTNfJth7jOqEHC+enpLeGslEhpnTAkg21GkRrWV4SsAtqchtT4YS9/nODBU2/ZfsBY2X4J/dX7A==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/eslint": { "version": "7.2.4", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-7.2.4.tgz", @@ -1820,6 +1913,47 @@ "integrity": "sha512-jnqIUKDUqJbDIUxm0Uj7bnlMnRm1T/eZ9N+AVMqhPgzrba2GhGG5o/jCTwmdPK709nEZsGoMzXEDUjcXHa3W0g==", "dev": true }, + "@types/etag": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@types/etag/-/etag-1.8.0.tgz", + "integrity": "sha512-EdSN0x+Y0/lBv7YAb8IU4Jgm6DWM+Bqtz7o5qozl96fzaqdqbdfHS5qjdpFeIv7xQ8jSLyjMMNShgYtMajEHyQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/express": { + "version": "4.17.9", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.9.tgz", + "integrity": "sha512-SDzEIZInC4sivGIFY4Sz1GG6J9UObPwCInYJjko2jzOf/Imx/dlpume6Xxwj1ORL82tBbmN4cPDIDkLbWHk9hw==", + "dev": true, + "requires": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "*", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "@types/express-serve-static-core": { + "version": "4.17.14", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.14.tgz", + "integrity": "sha512-uFTLwu94TfUFMToXNgRZikwPuZdOtDgs3syBtAIr/OXorL1kJqUJT9qCLnRZ5KBOWfZQikQ2xKgR2tnDj1OgDA==", + "dev": true, + "requires": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*" + } + }, + "@types/file-loader": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@types/file-loader/-/file-loader-4.2.0.tgz", + "integrity": "sha512-N3GMqKiKSNd41q4/lZlkdvNXKKWVdOXrA8Rniu64+25X0K2U1mWmTSu1CIqXKKsZUCwfaFcaioviLQtQ+EowLg==", + "dev": true, + "requires": { + "@types/webpack": "*" + } + }, "@types/graceful-fs": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.4.tgz", @@ -1829,6 +1963,17 @@ "@types/node": "*" } }, + "@types/html-minifier": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/html-minifier/-/html-minifier-4.0.0.tgz", + "integrity": "sha512-eFnGhrKmjWBlnSGNtunetE3UU2Tc/LUl92htFslSSTmpp9EKHQVcYQadCyYfnzUEFB5G/3wLWo/USQS/mEPKrA==", + "dev": true, + "requires": { + "@types/clean-css": "*", + "@types/relateurl": "*", + "@types/uglify-js": "*" + } + }, "@types/istanbul-lib-coverage": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz", @@ -1875,30 +2020,217 @@ "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", "dev": true }, + "@types/less": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/less/-/less-3.0.1.tgz", + "integrity": "sha512-dBp05MtWN/w1fGVjj5LVrDw6VrdYllpWczbUkCsrzBj08IHsSyRLOFvUrCFqZFVR+nsqkrRLNg6oOlvqMLPaSA==", + "dev": true + }, + "@types/memory-fs": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@types/memory-fs/-/memory-fs-0.3.2.tgz", + "integrity": "sha512-j5AcZo7dbMxHoOimcHEIh0JZe5e1b8q8AqGSpZJrYc7xOgCIP79cIjTdx5jSDLtySnQDwkDTqwlC7Xw7uXw7qg==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/mime": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.3.tgz", + "integrity": "sha512-Jus9s4CDbqwocc5pOAnh8ShfrnMcPHuJYzVcSUU7lrh8Ni5HuIqX3oilL86p3dlTrk0LzHRCgA/GQ7uNCw6l2Q==", + "dev": true + }, "@types/node": { "version": "14.14.6", "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.6.tgz", "integrity": "sha512-6QlRuqsQ/Ox/aJEQWBEJG7A9+u7oSYl3mem/K8IzxXG/kAGbV1YPD9Bg9Zw3vyxC/YP+zONKwy8hGkSt1jxFMw==", "dev": true }, + "@types/node-sass": { + "version": "4.11.1", + "resolved": "https://registry.npmjs.org/@types/node-sass/-/node-sass-4.11.1.tgz", + "integrity": "sha512-wPOmOEEtbwQiPTIgzUuRSQZ3H5YHinsxRGeZzPSDefAm4ylXWnZG9C0adses8ymyplKK0gwv3JkDNO8GGxnWfg==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/normalize-package-data": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz", "integrity": "sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA==", "dev": true }, + "@types/optimize-css-assets-webpack-plugin": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@types/optimize-css-assets-webpack-plugin/-/optimize-css-assets-webpack-plugin-5.0.1.tgz", + "integrity": "sha512-qyi5xmSl+DTmLFtVtelhso3VnNQYxltfgMa+Ed02xqNZCZBD0uYR6i64FmcwfieDzZRdwkJxt9o2JHq/5PBKQg==", + "dev": true, + "requires": { + "@types/webpack": "*" + } + }, "@types/prettier": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.1.5.tgz", "integrity": "sha512-UEyp8LwZ4Dg30kVU2Q3amHHyTn1jEdhCIE59ANed76GaT1Vp76DD3ZWSAxgCrw6wJ0TqeoBpqmfUHiUDPs//HQ==", "dev": true }, + "@types/pug": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/pug/-/pug-2.0.4.tgz", + "integrity": "sha1-h3L80EGOPNLMFxVV1zAHQVBR9LI=", + "dev": true + }, + "@types/qs": { + "version": "6.9.5", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.5.tgz", + "integrity": "sha512-/JHkVHtx/REVG0VVToGRGH2+23hsYLHdyG+GrvoUGlGAd0ErauXDyvHtRI/7H7mzLm+tBCKA7pfcpkQ1lf58iQ==", + "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/relateurl": { + "version": "0.2.28", + "resolved": "https://registry.npmjs.org/@types/relateurl/-/relateurl-0.2.28.tgz", + "integrity": "sha1-a9p9uGU/piZD9e5p6facEaOS46Y=", + "dev": true + }, + "@types/serve-static": { + "version": "1.13.8", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.8.tgz", + "integrity": "sha512-MoJhSQreaVoL+/hurAZzIm8wafFR6ajiTM1m4A0kv6AGeVBl4r4pOV8bGFrjjq1sGxDTnCoF8i22o0/aE5XCyA==", + "dev": true, + "requires": { + "@types/mime": "*", + "@types/node": "*" + } + }, + "@types/source-list-map": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.2.tgz", + "integrity": "sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA==", + "dev": true + }, "@types/stack-utils": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.0.tgz", "integrity": "sha512-RJJrrySY7A8havqpGObOB4W92QXKJo63/jFLLgpvOtsGUqbQZ9Sbgl35KMm1DjC6j7AvmmU2bIno+3IyEaemaw==", "dev": true }, + "@types/tapable": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/tapable/-/tapable-1.0.6.tgz", + "integrity": "sha512-W+bw9ds02rAQaMvaLYxAbJ6cvguW/iJXNT6lTssS1ps6QdrMKttqEAMEG/b5CR8TZl3/L7/lH0ZV5nNR1LXikA==", + "dev": true + }, + "@types/terser-webpack-plugin": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@types/terser-webpack-plugin/-/terser-webpack-plugin-2.2.1.tgz", + "integrity": "sha512-Z/6t/7qz4LeO64owJ9x7JQ6X791qfLxp1M1eCp6hFQlj7xuB5+Ol7DpEn5kWClTARZ7GlPLRsEWzFzQjZShF6w==", + "dev": true, + "requires": { + "@types/webpack": "*", + "terser": "^4.3.9" + }, + "dependencies": { + "terser": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-4.8.0.tgz", + "integrity": "sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw==", + "dev": true, + "requires": { + "commander": "^2.20.0", + "source-map": "~0.6.1", + "source-map-support": "~0.5.12" + } + } + } + }, + "@types/uglify-js": { + "version": "3.11.1", + "resolved": "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.11.1.tgz", + "integrity": "sha512-7npvPKV+jINLu1SpSYVWG8KvyJBhBa8tmzMMdDoVc2pWUYHN8KIXlPJhjJ4LT97c4dXJA2SHL/q6ADbDriZN+Q==", + "dev": true, + "requires": { + "source-map": "^0.6.1" + } + }, + "@types/url-template": { + "version": "2.0.28", + "resolved": "https://registry.npmjs.org/@types/url-template/-/url-template-2.0.28.tgz", + "integrity": "sha512-1i/YtOhvlWDbMDTWhCfvhyUwBS9vNFs78sJOyahoruJCcDbwaSH73AlnuCp7luKPm6qqdCg4VKq/IHUncl6gZA==", + "dev": true + }, + "@types/webpack": { + "version": "4.41.25", + "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.25.tgz", + "integrity": "sha512-cr6kZ+4m9lp86ytQc1jPOJXgINQyz3kLLunZ57jznW+WIAL0JqZbGubQk4GlD42MuQL5JGOABrxdpqqWeovlVQ==", + "dev": true, + "requires": { + "@types/anymatch": "*", + "@types/node": "*", + "@types/tapable": "*", + "@types/uglify-js": "*", + "@types/webpack-sources": "*", + "source-map": "^0.6.0" + } + }, + "@types/webpack-bundle-analyzer": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/@types/webpack-bundle-analyzer/-/webpack-bundle-analyzer-3.9.0.tgz", + "integrity": "sha512-O4Dsmml4T+emssdk3t6/N1vwtYRx1VfWCx0Oph4jRY62DZGNOL9IAS6mSX0XG1LdZuFSX0g42DXj1otQuPXRGQ==", + "dev": true, + "requires": { + "@types/webpack": "*" + } + }, + "@types/webpack-dev-middleware": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/@types/webpack-dev-middleware/-/webpack-dev-middleware-3.7.2.tgz", + "integrity": "sha512-PvETiS//pjVZBK48aJfbxzT7+9LIxanbnk9eXXYUfefGyPdsCkNrMDxRlOVrBvxukXUhD5B6N/pkPMdWrtuFkA==", + "dev": true, + "requires": { + "@types/connect": "*", + "@types/memory-fs": "*", + "@types/webpack": "*", + "loglevel": "^1.6.2" + } + }, + "@types/webpack-hot-middleware": { + "version": "2.25.3", + "resolved": "https://registry.npmjs.org/@types/webpack-hot-middleware/-/webpack-hot-middleware-2.25.3.tgz", + "integrity": "sha512-zGkTzrwQnhSadIXGYGZLu7tpXQwn4+6y9nGeql+5UeRtW/k54Jp4SnzB0Qw00ednw0ZFoZOvqTFfXSbFXohc5Q==", + "dev": true, + "requires": { + "@types/connect": "*", + "@types/webpack": "*" + } + }, + "@types/webpack-sources": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/webpack-sources/-/webpack-sources-2.0.0.tgz", + "integrity": "sha512-a5kPx98CNFRKQ+wqawroFunvFqv7GHm/3KOI52NY9xWADgc8smu4R6prt4EU/M4QfVjvgBkMqU4fBhw3QfMVkg==", + "dev": true, + "requires": { + "@types/node": "*", + "@types/source-list-map": "*", + "source-map": "^0.7.3" + }, + "dependencies": { + "source-map": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", + "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", + "dev": true + } + } + }, "@types/yargs": { "version": "15.0.9", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.9.tgz", @@ -1915,13 +2247,13 @@ "dev": true }, "@typescript-eslint/eslint-plugin": { - "version": "4.8.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.8.2.tgz", - "integrity": "sha512-gQ06QLV5l1DtvYtqOyFLXD9PdcILYqlrJj2l+CGDlPtmgLUzc1GpqciJFIRvyfvgLALpnxYINFuw+n9AZhPBKQ==", + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.9.0.tgz", + "integrity": "sha512-WrVzGMzzCrgrpnQMQm4Tnf+dk+wdl/YbgIgd5hKGa2P+lnJ2MON+nQnbwgbxtN9QDLi8HO+JAq0/krMnjQK6Cw==", "dev": true, "requires": { - "@typescript-eslint/experimental-utils": "4.8.2", - "@typescript-eslint/scope-manager": "4.8.2", + "@typescript-eslint/experimental-utils": "4.9.0", + "@typescript-eslint/scope-manager": "4.9.0", "debug": "^4.1.1", "functional-red-black-tree": "^1.0.1", "regexpp": "^3.0.0", @@ -1930,63 +2262,119 @@ }, "dependencies": { "semver": { - "version": "7.3.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", - "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==", - "dev": true + "version": "7.3.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.3.tgz", + "integrity": "sha512-kBGrn+sE2tyi6f4c9aFrrYRSTTF5yNOEVRBCdpcgykFp3jt2ZGlBwzIwWER9J9HZnQa9IF1TrR8Xy2UU+eaUhQ==", + "dev": true, + "requires": { + "lru-cache": "^4.1.5" + } } } }, "@typescript-eslint/experimental-utils": { - "version": "4.8.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.8.2.tgz", - "integrity": "sha512-hpTw6o6IhBZEsQsjuw/4RWmceRyESfAiEzAEnXHKG1X7S5DXFaZ4IO1JO7CW1aQ604leQBzjZmuMI9QBCAJX8Q==", + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.9.0.tgz", + "integrity": "sha512-0p8GnDWB3R2oGhmRXlEnCvYOtaBCijtA5uBfH5GxQKsukdSQyI4opC4NGTUb88CagsoNQ4rb/hId2JuMbzWKFQ==", "dev": true, "requires": { "@types/json-schema": "^7.0.3", - "@typescript-eslint/scope-manager": "4.8.2", - "@typescript-eslint/types": "4.8.2", - "@typescript-eslint/typescript-estree": "4.8.2", + "@typescript-eslint/scope-manager": "4.9.0", + "@typescript-eslint/types": "4.9.0", + "@typescript-eslint/typescript-estree": "4.9.0", "eslint-scope": "^5.0.0", "eslint-utils": "^2.0.0" } }, "@typescript-eslint/parser": { - "version": "4.8.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.8.2.tgz", - "integrity": "sha512-u0leyJqmclYr3KcXOqd2fmx6SDGBO0MUNHHAjr0JS4Crbb3C3d8dwAdlazy133PLCcPn+aOUFiHn72wcuc5wYw==", + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.9.0.tgz", + "integrity": "sha512-QRSDAV8tGZoQye/ogp28ypb8qpsZPV6FOLD+tbN4ohKUWHD2n/u0Q2tIBnCsGwQCiD94RdtLkcqpdK4vKcLCCw==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "4.8.2", - "@typescript-eslint/types": "4.8.2", - "@typescript-eslint/typescript-estree": "4.8.2", + "@typescript-eslint/scope-manager": "4.9.0", + "@typescript-eslint/types": "4.9.0", + "@typescript-eslint/typescript-estree": "4.9.0", "debug": "^4.1.1" + }, + "dependencies": { + "@typescript-eslint/scope-manager": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.9.0.tgz", + "integrity": "sha512-q/81jtmcDtMRE+nfFt5pWqO0R41k46gpVLnuefqVOXl4QV1GdQoBWfk5REcipoJNQH9+F5l+dwa9Li5fbALjzg==", + "dev": true, + "requires": { + "@typescript-eslint/types": "4.9.0", + "@typescript-eslint/visitor-keys": "4.9.0" + } + }, + "@typescript-eslint/types": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.9.0.tgz", + "integrity": "sha512-luzLKmowfiM/IoJL/rus1K9iZpSJK6GlOS/1ezKplb7MkORt2dDcfi8g9B0bsF6JoRGhqn0D3Va55b+vredFHA==", + "dev": true + }, + "@typescript-eslint/typescript-estree": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.9.0.tgz", + "integrity": "sha512-rmDR++PGrIyQzAtt3pPcmKWLr7MA+u/Cmq9b/rON3//t5WofNR4m/Ybft2vOLj0WtUzjn018ekHjTsnIyBsQug==", + "dev": true, + "requires": { + "@typescript-eslint/types": "4.9.0", + "@typescript-eslint/visitor-keys": "4.9.0", + "debug": "^4.1.1", + "globby": "^11.0.1", + "is-glob": "^4.0.1", + "lodash": "^4.17.15", + "semver": "^7.3.2", + "tsutils": "^3.17.1" + } + }, + "@typescript-eslint/visitor-keys": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.9.0.tgz", + "integrity": "sha512-sV45zfdRqQo1A97pOSx3fsjR+3blmwtdCt8LDrXgCX36v4Vmz4KHrhpV6Fo2cRdXmyumxx11AHw0pNJqCNpDyg==", + "dev": true, + "requires": { + "@typescript-eslint/types": "4.9.0", + "eslint-visitor-keys": "^2.0.0" + } + }, + "semver": { + "version": "7.3.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.3.tgz", + "integrity": "sha512-kBGrn+sE2tyi6f4c9aFrrYRSTTF5yNOEVRBCdpcgykFp3jt2ZGlBwzIwWER9J9HZnQa9IF1TrR8Xy2UU+eaUhQ==", + "dev": true, + "requires": { + "lru-cache": "^4.1.5" + } + } } }, "@typescript-eslint/scope-manager": { - "version": "4.8.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.8.2.tgz", - "integrity": "sha512-qHQ8ODi7mMin4Sq2eh/6eu03uVzsf5TX+J43xRmiq8ujng7ViQSHNPLOHGw/Wr5dFEoxq/ubKhzClIIdQy5q3g==", + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.9.0.tgz", + "integrity": "sha512-q/81jtmcDtMRE+nfFt5pWqO0R41k46gpVLnuefqVOXl4QV1GdQoBWfk5REcipoJNQH9+F5l+dwa9Li5fbALjzg==", "dev": true, "requires": { - "@typescript-eslint/types": "4.8.2", - "@typescript-eslint/visitor-keys": "4.8.2" + "@typescript-eslint/types": "4.9.0", + "@typescript-eslint/visitor-keys": "4.9.0" } }, "@typescript-eslint/types": { - "version": "4.8.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.8.2.tgz", - "integrity": "sha512-z1/AVcVF8ju5ObaHe2fOpZYEQrwHyZ7PTOlmjd3EoFeX9sv7UekQhfrCmgUO7PruLNfSHrJGQvrW3Q7xQ8EoAw==", + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.9.0.tgz", + "integrity": "sha512-luzLKmowfiM/IoJL/rus1K9iZpSJK6GlOS/1ezKplb7MkORt2dDcfi8g9B0bsF6JoRGhqn0D3Va55b+vredFHA==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "4.8.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.8.2.tgz", - "integrity": "sha512-HToGNwI6fekH0dOw3XEVESUm71Onfam0AKin6f26S2FtUmO7o3cLlWgrIaT1q3vjB3wCTdww3Dx2iGq5wtUOCg==", + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.9.0.tgz", + "integrity": "sha512-rmDR++PGrIyQzAtt3pPcmKWLr7MA+u/Cmq9b/rON3//t5WofNR4m/Ybft2vOLj0WtUzjn018ekHjTsnIyBsQug==", "dev": true, "requires": { - "@typescript-eslint/types": "4.8.2", - "@typescript-eslint/visitor-keys": "4.8.2", + "@typescript-eslint/types": "4.9.0", + "@typescript-eslint/visitor-keys": "4.9.0", "debug": "^4.1.1", "globby": "^11.0.1", "is-glob": "^4.0.1", @@ -1996,20 +2384,23 @@ }, "dependencies": { "semver": { - "version": "7.3.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", - "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==", - "dev": true + "version": "7.3.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.3.tgz", + "integrity": "sha512-kBGrn+sE2tyi6f4c9aFrrYRSTTF5yNOEVRBCdpcgykFp3jt2ZGlBwzIwWER9J9HZnQa9IF1TrR8Xy2UU+eaUhQ==", + "dev": true, + "requires": { + "lru-cache": "^4.1.5" + } } } }, "@typescript-eslint/visitor-keys": { - "version": "4.8.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.8.2.tgz", - "integrity": "sha512-Vg+/SJTMZJEKKGHW7YC21QxgKJrSbxoYYd3MEUGtW7zuytHuEcksewq0DUmo4eh/CTNrVJGSdIY9AtRb6riWFw==", + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.9.0.tgz", + "integrity": "sha512-sV45zfdRqQo1A97pOSx3fsjR+3blmwtdCt8LDrXgCX36v4Vmz4KHrhpV6Fo2cRdXmyumxx11AHw0pNJqCNpDyg==", "dev": true, "requires": { - "@typescript-eslint/types": "4.8.2", + "@typescript-eslint/types": "4.9.0", "eslint-visitor-keys": "^2.0.0" } }, @@ -7360,6 +7751,12 @@ "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=", "dev": true }, + "loglevel": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.7.1.tgz", + "integrity": "sha512-Hesni4s5UkWkwCGJMQGAh71PaLUmKFM60dHvq0zi/vDhhrzuk+4GgNbTXJ12YYQJn6ZKBDNIjYcuQGKudvqrIw==", + "dev": true + }, "lru-cache": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", @@ -8183,6 +8580,80 @@ "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", "dev": true }, + "postcss": { + "version": "7.0.35", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.35.tgz", + "integrity": "sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "dependencies": { + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, "prelude-ls": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", diff --git a/package.json b/package.json index 33c5a7b9..84edbd5e 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "clean": "cross-env rimraf dist coverage lib", "coverage": "cross-env npm run build && jest --coverage", "lint": "cross-env eslint src --ext .js,.ts", - "test": "cross-env npm run build && jest" + "test": "cross-env npm run lint && npm run build && jest" }, "engines": { "node": ">=0.12" @@ -40,8 +40,10 @@ "@babel/plugin-transform-runtime": "^7.12.1", "@babel/preset-env": "^7.12.1", "@babel/preset-typescript": "^7.12.7", - "@typescript-eslint/eslint-plugin": "^4.8.2", - "@typescript-eslint/parser": "^4.8.2", + "@nuxt/types": "^2.14.7", + "@types/url-template": "^2.0.28", + "@typescript-eslint/eslint-plugin": "^4.9.0", + "@typescript-eslint/parser": "^4.9.0", "@vue/eslint-config-standard": "^5.1.2", "@vue/test-utils": "^1.1.1", "axios": "^0.21.0", @@ -72,4 +74,4 @@ "webpack-cli": "^4.2.0", "webpack-node-externals": "^2.5.2" } -} +} \ No newline at end of file diff --git a/src/QueryablePromise.js b/src/QueryablePromise.ts similarity index 94% rename from src/QueryablePromise.js rename to src/QueryablePromise.ts index d824db6f..5484be20 100644 --- a/src/QueryablePromise.js +++ b/src/QueryablePromise.ts @@ -3,8 +3,8 @@ * Based on: http://stackoverflow.com/questions/21485545/is-there-a-way-to-tell-if-an-es6-promise-is-fulfilled-rejected-resolved * But modified according to the specs of promises : https://promisesaplus.com/ */ -class QueryablePromise { - constructor (promise) { +class QueryablePromise { + constructor (promise: Promise) { // Don't modify any promise that has been already modified if (Symbol.for('isPending') in promise) return promise @@ -38,7 +38,7 @@ class QueryablePromise { /** * Returns a resolved Promise and immediately mark it as 'done' */ - static resolve (value) { + static resolve (value: T): Promise { const promise = Promise.resolve(value) promise[Symbol.for('isFulfilled')] = true diff --git a/src/ServerException.ts b/src/ServerException.ts index 159f6d4e..568c1a32 100644 --- a/src/ServerException.ts +++ b/src/ServerException.ts @@ -10,8 +10,8 @@ export default class ServerException extends Error { * @param response Axios reponse object * @param params Standard Error parameters */ - public constructor (response: AxiosResponse, ...params: any[]) { - super(...params) + public constructor (response: AxiosResponse, message?: string) { + super(message) if (!this.message) { this.message = 'Server error ' + response.status + ' (' + response.statusText + ')' diff --git a/src/StoreValue.js b/src/StoreValue.js deleted file mode 100644 index b2c4ef7e..00000000 --- a/src/StoreValue.js +++ /dev/null @@ -1,66 +0,0 @@ -import urltemplate from 'url-template' -import { isTemplatedLink, isEntityReference, isCollection } from './halHelpers.js' -import QueryablePromise from './QueryablePromise.js' -import EmbeddedCollection from './EmbeddedCollection.js' -import CanHaveItems from './CanHaveItems.js' - -/** - * Creates an actual StoreValue, by wrapping the given Vuex store data. The data must not be loading. - * If the data has been loaded into the store before but is currently reloading, the old data will be - * returned, along with a ._meta.load promise that resolves when the reload is complete. - * @param data fully loaded entity data from the Vuex store - */ -class StoreValue extends CanHaveItems { - constructor (data, { get, reload, post, patch, del, isUnknown }, StoreValueCreator, config) { - super({ get, reload, isUnknown }, config) - - this.apiActions = { get, reload, post, patch, del, isUnknown } - this.config = config - - Object.keys(data).forEach(key => { - const value = data[key] - if (key === 'allItems' && isCollection(data)) return - if (key === 'items' && isCollection(data)) { - this.addItemsGetter(data[key], data._meta.self, key) - } else if (Array.isArray(value)) { - this[key] = () => new EmbeddedCollection(value, data._meta.self, key, { get, reload, isUnknown }, config, data._meta.load) - } else if (isEntityReference(value)) { - this[key] = () => this.apiActions.get(value.href) - } else if (isTemplatedLink(value)) { - this[key] = templateParams => this.apiActions.get(urltemplate.parse(value.href).expand(templateParams || {})) - } else { - this[key] = value - } - }) - - // Use a trivial load promise to break endless recursion, except if we are currently reloading the data from the API - const loadedPromise = data._meta.load && !data._meta.load[Symbol.for('done')] - ? data._meta.load.then(reloadedData => StoreValueCreator.wrap(reloadedData)) - : QueryablePromise.resolve(this) - - // Use a shallow clone of _meta, since we don't want to overwrite the ._meta.load promise or self link in the Vuex store - this._meta = { ...data._meta, load: loadedPromise, self: this.config.apiRoot + data._meta.self } - } - - $reload () { - return this.apiActions.reload(this._meta.self) - } - - $loadItems () { - return this._meta.load - } - - $post (data) { - return this.apiActions.post(this._meta.self, data) - } - - $patch (data) { - return this.apiActions.patch(this._meta.self, data) - } - - $del () { - return this.apiActions.del(this._meta.self) - } -} - -export default StoreValue diff --git a/src/StoreValue.ts b/src/StoreValue.ts new file mode 100644 index 00000000..af54904e --- /dev/null +++ b/src/StoreValue.ts @@ -0,0 +1,97 @@ +import urltemplate from 'url-template' +import { isTemplatedLink, isEntityReference, isCollection } from './halHelpers.js' +import QueryablePromise from './QueryablePromise' +import EmbeddedCollection from './EmbeddedCollection.js' +import CanHaveItems from './CanHaveItems.js' +import Resource from './interfaces/Resource' +import ApiActions from './interfaces/ApiActions' +import StoreData from './interfaces/StoreData' +import StoreValueCreator from './StoreValueCreator.js' +import { InternalConfig } from './interfaces/Config.js' + +/** + * Creates an actual StoreValue, by wrapping the given Vuex store storeData. The storeData must not be loading. + * If the storeData has been loaded into the store before but is currently reloading, the old storeData will be + * returned, along with a ._meta.load promise that resolves when the reload is complete. + * @param storeData fully loaded entity storeData from the Vuex store + */ +class StoreValue extends CanHaveItems implements Resource { + public _meta: { + self: string, + load: Promise + } + + private storeData: StoreData + config: InternalConfig + apiActions: ApiActions + + constructor (storeData: StoreData, { get, reload, post, patch, del, isUnknown }: ApiActions, storeValueCreator: StoreValueCreator, config: InternalConfig) { + super({ get, reload, isUnknown }, config) + + this.apiActions = { get, reload, post, patch, del, isUnknown } + this.config = config + this.storeData = storeData + + Object.keys(storeData).forEach(key => { + const value = storeData[key] + + // TODO: Why is the next line needed.? Store data should never contain a property 'allItems'. Or can it? + if (key === 'allItems' && isCollection(storeData)) return + + // storeData is a collection: add keys to retrieve collection items + if (key === 'items' && isCollection(storeData)) { + this.addItemsGetter(storeData[key], storeData._meta.self, key) + + // storeData[key] is an embedded collection + } else if (Array.isArray(value)) { + this[key] = () => new EmbeddedCollection(value, storeData._meta.self, key, { get, reload, isUnknown }, config, storeData._meta.load) + + // storeData[key] is a reference only (contains only href; no data) + } else if (isEntityReference(value)) { + this[key] = () => this.apiActions.get(value.href) + + // storeData[key] is a templated link + } else if (isTemplatedLink(value)) { + this[key] = templateParams => this.apiActions.get(urltemplate.parse(value.href).expand(templateParams || {})) + + // storeData[key] is a primitive (normal entity property) + } else { + this[key] = value + } + }) + + // Use a trivial load promise to break endless recursion, except if we are currently reloading the storeData from the API + const loadPromise = storeData._meta.load && !storeData._meta.load[Symbol.for('done')] + ? storeData._meta.load.then(reloadedData => storeValueCreator.wrap(reloadedData)) + : QueryablePromise.resolve(this) + + // Use a shallow clone of _meta, since we don't want to overwrite the ._meta.load promise or self link in the Vuex store + this._meta = { + ...storeData._meta, + load: loadPromise as Promise, + self: this.config.apiRoot + storeData._meta.self + } + } + + $reload (): Promise { + return this.apiActions.reload(this._meta.self) + } + + $loadItems (): Promise { + return this._meta.load + } + + $post (data: unknown): Promise { + return this.apiActions.post(this._meta.self, data) + } + + $patch (data: unknown): Promise { + return this.apiActions.patch(this._meta.self, data) + } + + $del (): Promise { + return this.apiActions.del(this._meta.self) + } +} + +export default StoreValue diff --git a/src/StoreValueCreator.js b/src/StoreValueCreator.js index fdf9a291..a1a8f41c 100644 --- a/src/StoreValueCreator.js +++ b/src/StoreValueCreator.js @@ -1,4 +1,4 @@ -import StoreValue from './StoreValue.js' +import StoreValue from './StoreValue.ts' import LoadingStoreValue from './LoadingStoreValue.js' class StoreValueCreator { diff --git a/src/index.js b/src/index.js index 17e4711d..d52060f3 100644 --- a/src/index.js +++ b/src/index.js @@ -2,7 +2,7 @@ import normalize from 'hal-json-normalizer' import urltemplate from 'url-template' import { normalizeEntityUri } from './normalizeUri' import StoreValueCreator from './StoreValueCreator' -import StoreValue from './StoreValue' +import StoreValue from './StoreValue.ts' import LoadingStoreValue from './LoadingStoreValue' import storeModule from './storeModule' import ServerException from './ServerException.ts' diff --git a/src/interfaces/ApiActions.ts b/src/interfaces/ApiActions.ts new file mode 100644 index 00000000..ccbae228 --- /dev/null +++ b/src/interfaces/ApiActions.ts @@ -0,0 +1,12 @@ +import Resource from './Resource' + +interface ApiActions { + get: (uriOrEntity: string | Resource, forceReload?: boolean) => Resource + reload: (uriOrEntity: string | Resource) => Promise + post: (uriOrEntity: string | Resource, data: unknown) => Promise + patch: (uriOrEntity: string | Resource, data: unknown) => Promise + del: (uriOrEntity: string | Resource) => Promise + isUnknown: (uri: string) => boolean +} + +export default ApiActions diff --git a/src/interfaces/Config.ts b/src/interfaces/Config.ts new file mode 100644 index 00000000..8de868a4 --- /dev/null +++ b/src/interfaces/Config.ts @@ -0,0 +1,15 @@ +import { Inject } from '@nuxt/types/app' + +interface ExternalConfig { + apiName?: string + avoidNPlusOneRequests?: boolean + forceRequestedSelfLink?: boolean + nuxtInject?: Inject +} + +interface InternalConfig extends ExternalConfig { + apiRoot?: string +} + +export { InternalConfig, ExternalConfig } +export default ExternalConfig diff --git a/src/interfaces/Resource.ts b/src/interfaces/Resource.ts new file mode 100644 index 00000000..d91674e7 --- /dev/null +++ b/src/interfaces/Resource.ts @@ -0,0 +1,16 @@ +interface Resource { + _meta: { + self: string + load: Promise + } + + $reload: () => Promise + $loadItems: () => Promise + $post: (data: unknown) => Promise + $patch: (data: unknown) => Promise + $del: () => Promise + + items?: Array +} + +export default Resource diff --git a/src/interfaces/StoreData.ts b/src/interfaces/StoreData.ts new file mode 100644 index 00000000..2d3f760c --- /dev/null +++ b/src/interfaces/StoreData.ts @@ -0,0 +1,10 @@ +import Resource from './Resource' + +interface StoreData { + _meta: { + self: string + load: Promise + } +} + +export default StoreData diff --git a/tsconfig.json b/tsconfig.json index 92690e5d..73922eb5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,10 +1,14 @@ { "compilerOptions": { - "target": "es2018", - "module": "esnext", + "lib": [ + "ES2019" + ], + "module": "commonjs", + "target": "ES2019", "strict": true, "importHelpers": true, "moduleResolution": "node", + "allowJs": true, "esModuleInterop": true, "allowSyntheticDefaultImports": true, "baseUrl": ".", @@ -14,18 +18,13 @@ "declaration": true, "declarationMap": true, "removeComments": false, - "noImplicitAny": true, + "noImplicitAny": false, "noImplicitReturns": true, "noImplicitThis": true, - "noUnusedLocals": true, + "noUnusedLocals": false, "noUnusedParameters": true, "strictNullChecks": true, "suppressImplicitAnyIndexErrors": true, - "lib": [ - "esnext", - "dom", - "es2017" - ], "paths": { "@/*": [ "src/*" diff --git a/webpack.config.js b/webpack.config.js index 97232c87..81ea05b1 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -22,8 +22,13 @@ module.exports = { ], module: { rules: [ + // All files with a '.ts' or '.tsx' extension will be handled by 'ts-loader'. - { test: /\.tsx?$/, loader: 'ts-loader' }, + { + test: /\.tsx?$/, + use: 'ts-loader', + exclude: /node_modules/ + }, // All output '.js' files will have any sourcemaps re-processed by 'source-map-loader'. { test: /\.js$/, loader: 'source-map-loader' }, @@ -33,7 +38,7 @@ module.exports = { }, resolve: { // Add '.ts' and '.tsx' as resolvable extensions. - // extensions: ['', '.webpack.js', '.web.js', '.ts', '.tsx', '.js'], + extensions: ['.tsx', '.ts', '.js'], modules: [ path.join(__dirname, 'src'), From f69286af9b07f3d3709da290da9534517c9beff2 Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Sat, 5 Dec 2020 08:57:23 +0100 Subject: [PATCH 004/120] require QueryablePromise consistently; override .then/.catch --- src/QueryablePromise.ts | 35 ++++++++++++++++++++++++++++------- src/StoreValue.ts | 21 +++++++++------------ src/interfaces/ApiActions.ts | 9 +++++---- src/interfaces/Resource.ts | 14 ++++++++------ src/interfaces/StoreData.ts | 3 ++- 5 files changed, 52 insertions(+), 30 deletions(-) diff --git a/src/QueryablePromise.ts b/src/QueryablePromise.ts index 5484be20..a8808acd 100644 --- a/src/QueryablePromise.ts +++ b/src/QueryablePromise.ts @@ -4,9 +4,13 @@ * But modified according to the specs of promises : https://promisesaplus.com/ */ class QueryablePromise { + private originalPromise: Promise + constructor (promise: Promise) { // Don't modify any promise that has been already modified - if (Symbol.for('isPending') in promise) return promise + // if (Symbol.for('isPending') in promise) return promise + + this.originalPromise = promise // Set initial state let isPending = true @@ -27,24 +31,41 @@ class QueryablePromise { } ) + Object.defineProperty(queryablePromise, 'originalPromise', promise) + Object.defineProperty(queryablePromise, Symbol.for('isFulfilled'), { get: function () { return isFulfilled } }) Object.defineProperty(queryablePromise, Symbol.for('isPending'), { get: function () { return isPending } }) Object.defineProperty(queryablePromise, Symbol.for('isRejected'), { get: function () { return isRejected } }) Object.defineProperty(queryablePromise, Symbol.for('done'), { get: function () { return !isPending } }) // official terminology would be 'isSettled' or 'isResolved' (https://stackoverflow.com/questions/29268569/what-is-the-correct-terminology-for-javascript-promises) + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore return queryablePromise } + then (onfulfilled?: ((value: T) => TResult1 | PromiseLike) | null | undefined, onrejected?: ((reason: unknown) => TResult2 | PromiseLike) | null | undefined): QueryablePromise { + return new QueryablePromise(this.originalPromise.then(onfulfilled, onrejected)) + } + + catch (onrejected?: ((reason: unknown) => TResult | PromiseLike) | null | undefined) : QueryablePromise { + return new QueryablePromise(this.originalPromise.catch(onrejected)) + } + /** * Returns a resolved Promise and immediately mark it as 'done' */ - static resolve (value: T): Promise { - const promise = Promise.resolve(value) + static resolve (value: T): QueryablePromise { + let promiseResolve: (value: T) => void + + // create a new QueryablePromise... + const promise = new QueryablePromise(new Promise(function (resolve) { + promiseResolve = resolve + }) as Promise) - promise[Symbol.for('isFulfilled')] = true - promise[Symbol.for('isPending')] = false - promise[Symbol.for('isRejected')] = false - promise[Symbol.for('done')] = true + // .. and resolve it immediately (although Typescript is not very happy with this: https://github.com/microsoft/TypeScript/issues/36968) + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + promiseResolve(value) return promise } diff --git a/src/StoreValue.ts b/src/StoreValue.ts index af54904e..7abcdc92 100644 --- a/src/StoreValue.ts +++ b/src/StoreValue.ts @@ -18,7 +18,7 @@ import { InternalConfig } from './interfaces/Config.js' class StoreValue extends CanHaveItems implements Resource { public _meta: { self: string, - load: Promise + load: QueryablePromise } private storeData: StoreData @@ -35,9 +35,6 @@ class StoreValue extends CanHaveItems implements Resource { Object.keys(storeData).forEach(key => { const value = storeData[key] - // TODO: Why is the next line needed.? Store data should never contain a property 'allItems'. Or can it? - if (key === 'allItems' && isCollection(storeData)) return - // storeData is a collection: add keys to retrieve collection items if (key === 'items' && isCollection(storeData)) { this.addItemsGetter(storeData[key], storeData._meta.self, key) @@ -62,34 +59,34 @@ class StoreValue extends CanHaveItems implements Resource { // Use a trivial load promise to break endless recursion, except if we are currently reloading the storeData from the API const loadPromise = storeData._meta.load && !storeData._meta.load[Symbol.for('done')] - ? storeData._meta.load.then(reloadedData => storeValueCreator.wrap(reloadedData)) - : QueryablePromise.resolve(this) + ? storeData._meta.load.then(reloadedData => (storeValueCreator.wrap(reloadedData) as Resource)) + : QueryablePromise.resolve(this as Resource) // Use a shallow clone of _meta, since we don't want to overwrite the ._meta.load promise or self link in the Vuex store this._meta = { ...storeData._meta, - load: loadPromise as Promise, + load: loadPromise as QueryablePromise, self: this.config.apiRoot + storeData._meta.self } } - $reload (): Promise { + $reload (): QueryablePromise { return this.apiActions.reload(this._meta.self) } - $loadItems (): Promise { + $loadItems (): QueryablePromise { return this._meta.load } - $post (data: unknown): Promise { + $post (data: unknown): QueryablePromise { return this.apiActions.post(this._meta.self, data) } - $patch (data: unknown): Promise { + $patch (data: unknown): QueryablePromise { return this.apiActions.patch(this._meta.self, data) } - $del (): Promise { + $del (): QueryablePromise { return this.apiActions.del(this._meta.self) } } diff --git a/src/interfaces/ApiActions.ts b/src/interfaces/ApiActions.ts index ccbae228..214ae08f 100644 --- a/src/interfaces/ApiActions.ts +++ b/src/interfaces/ApiActions.ts @@ -1,11 +1,12 @@ import Resource from './Resource' +import QueryablePromise from '../QueryablePromise' interface ApiActions { get: (uriOrEntity: string | Resource, forceReload?: boolean) => Resource - reload: (uriOrEntity: string | Resource) => Promise - post: (uriOrEntity: string | Resource, data: unknown) => Promise - patch: (uriOrEntity: string | Resource, data: unknown) => Promise - del: (uriOrEntity: string | Resource) => Promise + reload: (uriOrEntity: string | Resource) => QueryablePromise + post: (uriOrEntity: string | Resource, data: unknown) => QueryablePromise + patch: (uriOrEntity: string | Resource, data: unknown) => QueryablePromise + del: (uriOrEntity: string | Resource) => QueryablePromise isUnknown: (uri: string) => boolean } diff --git a/src/interfaces/Resource.ts b/src/interfaces/Resource.ts index d91674e7..a285434c 100644 --- a/src/interfaces/Resource.ts +++ b/src/interfaces/Resource.ts @@ -1,14 +1,16 @@ +import QueryablePromise from '../QueryablePromise' + interface Resource { _meta: { self: string - load: Promise + load: QueryablePromise } - $reload: () => Promise - $loadItems: () => Promise - $post: (data: unknown) => Promise - $patch: (data: unknown) => Promise - $del: () => Promise + $reload: () => QueryablePromise + $loadItems: () => QueryablePromise + $post: (data: unknown) => QueryablePromise + $patch: (data: unknown) => QueryablePromise + $del: () => QueryablePromise items?: Array } diff --git a/src/interfaces/StoreData.ts b/src/interfaces/StoreData.ts index 2d3f760c..a34bf7a0 100644 --- a/src/interfaces/StoreData.ts +++ b/src/interfaces/StoreData.ts @@ -1,9 +1,10 @@ import Resource from './Resource' +import QueryablePromise from '../QueryablePromise' interface StoreData { _meta: { self: string - load: Promise + load: QueryablePromise } } From 81435c1f174c5416540181f3feaab1b5fdeefde5 Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Sat, 5 Dec 2020 12:16:09 +0100 Subject: [PATCH 005/120] refactor QueryablePromise --- package.json | 3 +- src/QueryablePromise.ts | 117 ++++++++++++++++++++++------------------ src/StoreValue.ts | 10 ++-- src/index.js | 12 ++--- 4 files changed, 77 insertions(+), 65 deletions(-) diff --git a/package.json b/package.json index 84edbd5e..59542325 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,8 @@ "clean": "cross-env rimraf dist coverage lib", "coverage": "cross-env npm run build && jest --coverage", "lint": "cross-env eslint src --ext .js,.ts", - "test": "cross-env npm run lint && npm run build && jest" + "test": "cross-env npm run lint && npm run build && jest", + "test:debug": "node --inspect-brk ./node_modules/.bin/jest -i" }, "engines": { "node": ">=0.12" diff --git a/src/QueryablePromise.ts b/src/QueryablePromise.ts index a8808acd..70372d33 100644 --- a/src/QueryablePromise.ts +++ b/src/QueryablePromise.ts @@ -3,72 +3,83 @@ * Based on: http://stackoverflow.com/questions/21485545/is-there-a-way-to-tell-if-an-es6-promise-is-fulfilled-rejected-resolved * But modified according to the specs of promises : https://promisesaplus.com/ */ -class QueryablePromise { - private originalPromise: Promise - constructor (promise: Promise) { - // Don't modify any promise that has been already modified - // if (Symbol.for('isPending') in promise) return promise +interface QueryablePromise extends Promise { + isFulfilled: () => boolean + isPending: () => boolean + isRejected: () => boolean - this.originalPromise = promise - - // Set initial state - let isPending = true - let isRejected = false - let isFulfilled = false + /* + // then and catch return QueryablePromise instead of a Promise + then: (onfulfilled?: ((value: T) => TResult1 | PromiseLike) | undefined | null, onrejected?: ((reason: unknown) => TResult2 | PromiseLike) | undefined | null) => QueryablePromise + catch: (onrejected?: ((reason: unknown) => TResult | PromiseLike) | null | undefined) => QueryablePromise + */ +} - // Observe the promise, saving the fulfillment in a closure scope. - const queryablePromise = promise.then( - function (v) { - isFulfilled = true - isPending = false - return v - }, - function (e) { - isRejected = true - isPending = false - throw e - } - ) +/** + * Wraps a promise to exposes its internal status + * @param promise + */ +function wrapPromise (promise: Promise): QueryablePromise { + // Don't wrap any promise that has been already wrapped + if ('isPending' in promise) return promise - Object.defineProperty(queryablePromise, 'originalPromise', promise) + // Set initial state + let isPending = true + let isRejected = false + let isFulfilled = false - Object.defineProperty(queryablePromise, Symbol.for('isFulfilled'), { get: function () { return isFulfilled } }) - Object.defineProperty(queryablePromise, Symbol.for('isPending'), { get: function () { return isPending } }) - Object.defineProperty(queryablePromise, Symbol.for('isRejected'), { get: function () { return isRejected } }) - Object.defineProperty(queryablePromise, Symbol.for('done'), { get: function () { return !isPending } }) // official terminology would be 'isSettled' or 'isResolved' (https://stackoverflow.com/questions/29268569/what-is-the-correct-terminology-for-javascript-promises) + // Observe the promise, saving the fulfillment in a closure scope. + const queryablePromise = promise.then( + function (v) { + isFulfilled = true + isPending = false + return v + }, + function (e) { + isRejected = true + isPending = false + throw e + } + ) as QueryablePromise - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - return queryablePromise + /* + // override .then to chain original promise and wrap again + queryablePromise.then = function (onfulfilled?: ((value: T) => TResult1 | PromiseLike) | undefined | null, onrejected?: ((reason: unknown) => TResult2 | PromiseLike) | undefined | null): QueryablePromise { + return wrapPromise(promise.then(onfulfilled, onrejected)) } - then (onfulfilled?: ((value: T) => TResult1 | PromiseLike) | null | undefined, onrejected?: ((reason: unknown) => TResult2 | PromiseLike) | null | undefined): QueryablePromise { - return new QueryablePromise(this.originalPromise.then(onfulfilled, onrejected)) - } + // override .catch to chain original promise and wrap again + queryablePromise.catch = function (onrejected?: ((reason: unknown) => TResult | PromiseLike) | null | undefined) : QueryablePromise { + return wrapPromise(promise.catch(onrejected)) + } */ - catch (onrejected?: ((reason: unknown) => TResult | PromiseLike) | null | undefined) : QueryablePromise { - return new QueryablePromise(this.originalPromise.catch(onrejected)) - } + queryablePromise.isFulfilled = function () { return isFulfilled } + queryablePromise.isPending = function () { return isPending } + queryablePromise.isRejected = function () { return isRejected } + Object.defineProperty(queryablePromise, Symbol.for('done'), { get: function () { return !isPending } }) // official terminology would be 'isSettled' or 'isResolved' (https://stackoverflow.com/questions/29268569/what-is-the-correct-terminology-for-javascript-promises) - /** - * Returns a resolved Promise and immediately mark it as 'done' - */ - static resolve (value: T): QueryablePromise { - let promiseResolve: (value: T) => void + return queryablePromise +} - // create a new QueryablePromise... - const promise = new QueryablePromise(new Promise(function (resolve) { - promiseResolve = resolve - }) as Promise) +/** + * Returns a resolved Promise and immediately mark it as 'done' + */ +function createResolvedPromise (value: T): QueryablePromise { + let promiseResolve: (value: T) => void - // .. and resolve it immediately (although Typescript is not very happy with this: https://github.com/microsoft/TypeScript/issues/36968) - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - promiseResolve(value) + // create a new QueryablePromise... + const promise = wrapPromise(new Promise(function (resolve) { + promiseResolve = resolve + }) as Promise) - return promise - } + // .. and resolve it immediately (although Typescript is not very happy with this: https://github.com/microsoft/TypeScript/issues/36968) + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + promiseResolve(value) + + return promise } +export { QueryablePromise, wrapPromise, createResolvedPromise } export default QueryablePromise diff --git a/src/StoreValue.ts b/src/StoreValue.ts index 7abcdc92..144811cf 100644 --- a/src/StoreValue.ts +++ b/src/StoreValue.ts @@ -1,6 +1,6 @@ import urltemplate from 'url-template' import { isTemplatedLink, isEntityReference, isCollection } from './halHelpers.js' -import QueryablePromise from './QueryablePromise' +import { QueryablePromise, createResolvedPromise, wrapPromise } from './QueryablePromise' import EmbeddedCollection from './EmbeddedCollection.js' import CanHaveItems from './CanHaveItems.js' import Resource from './interfaces/Resource' @@ -58,14 +58,14 @@ class StoreValue extends CanHaveItems implements Resource { }) // Use a trivial load promise to break endless recursion, except if we are currently reloading the storeData from the API - const loadPromise = storeData._meta.load && !storeData._meta.load[Symbol.for('done')] - ? storeData._meta.load.then(reloadedData => (storeValueCreator.wrap(reloadedData) as Resource)) - : QueryablePromise.resolve(this as Resource) + const loadPromise = storeData._meta.load && storeData._meta.load.isPending() + ? wrapPromise(storeData._meta.load.then(reloadedData => (storeValueCreator.wrap(reloadedData) as Resource))) + : createResolvedPromise(this) // Use a shallow clone of _meta, since we don't want to overwrite the ._meta.load promise or self link in the Vuex store this._meta = { ...storeData._meta, - load: loadPromise as QueryablePromise, + load: loadPromise, self: this.config.apiRoot + storeData._meta.self } } diff --git a/src/index.js b/src/index.js index d52060f3..dba304e0 100644 --- a/src/index.js +++ b/src/index.js @@ -6,7 +6,7 @@ import StoreValue from './StoreValue.ts' import LoadingStoreValue from './LoadingStoreValue' import storeModule from './storeModule' import ServerException from './ServerException.ts' -import QueryablePromise from './QueryablePromise' +import { createResolvedPromise, wrapPromise } from './QueryablePromise' /** * Defines the API store methods available in all Vue components. The methods can be called as follows: @@ -60,7 +60,7 @@ function HalJsonVuex (store, axios, options) { if (uri === null) { return Promise.reject(new Error(`Could not perform POST, "${uriOrCollection}" is not an entity or URI`)) } - return new QueryablePromise(axios.post(axios.defaults.baseURL + uri, preparePostData(data)).then(({ data }) => { + return wrapPromise(axios.post(axios.defaults.baseURL + uri, preparePostData(data)).then(({ data }) => { storeHalJsonData(data) return get(data._links.self.href) }, (error) => { @@ -75,7 +75,7 @@ function HalJsonVuex (store, axios, options) { * @returns Promise Resolves when the GET request has completed and the updated entity is available * in the Vuex store. */ - function reload (uriOrEntity) { + async function reload (uriOrEntity) { return get(uriOrEntity, true)._meta.load } @@ -235,7 +235,7 @@ function HalJsonVuex (store, axios, options) { store.commit('addEmpty', uri) } - store.state[opts.apiName][uri]._meta.load = new QueryablePromise(axios.patch(axios.defaults.baseURL + uri, data).then(({ data }) => { + store.state[opts.apiName][uri]._meta.load = wrapPromise(axios.patch(axios.defaults.baseURL + uri, data).then(({ data }) => { if (opts.forceRequestedSelfLink) { data._links.self.href = uri } @@ -291,7 +291,7 @@ function HalJsonVuex (store, axios, options) { return Promise.reject(new Error(`Could not perform DELETE, "${uriOrEntity}" is not an entity or URI`)) } store.commit('deleting', uri) - return new QueryablePromise(axios.delete(axios.defaults.baseURL + uri).then( + return wrapPromise(axios.delete(axios.defaults.baseURL + uri).then( () => deleted(uri), (error) => { store.commit('deletingFailed', uri) @@ -363,7 +363,7 @@ function HalJsonVuex (store, axios, options) { * @param promise */ function setLoadPromiseOnStore (uri, promise = null) { - store.state[opts.apiName][uri]._meta.load = promise ? new QueryablePromise(promise) : QueryablePromise.resolve(store.state[opts.apiName][uri]) + store.state[opts.apiName][uri]._meta.load = promise ? wrapPromise(promise) : createResolvedPromise(store.state[opts.apiName][uri]) } /** From d5851755e7ce8d038dfd488c2a153693667d27cf Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Sat, 5 Dec 2020 17:31:57 +0100 Subject: [PATCH 006/120] convert LoadingStoreValue to Typescript --- src/LoadingStoreValue.js | 70 ------------- src/LoadingStoreValue.ts | 98 +++++++++++++++++++ src/StoreValue.ts | 3 +- ...reValueCreator.js => StoreValueCreator.ts} | 18 +++- src/interfaces/Resource.ts | 3 +- src/interfaces/StoreData.ts | 4 +- tests/store.spec.js | 5 +- 7 files changed, 120 insertions(+), 81 deletions(-) delete mode 100644 src/LoadingStoreValue.js create mode 100644 src/LoadingStoreValue.ts rename src/{StoreValueCreator.js => StoreValueCreator.ts} (70%) diff --git a/src/LoadingStoreValue.js b/src/LoadingStoreValue.js deleted file mode 100644 index e01c9f1e..00000000 --- a/src/LoadingStoreValue.js +++ /dev/null @@ -1,70 +0,0 @@ -import LoadingStoreCollection from './LoadingStoreCollection' - -/** - * Creates a placeholder for an entity which has not yet finished loading from the API. - * Such a LoadingStoreValue can safely be used in Vue components, since it will render as an empty - * string and Vue's reactivity system will replace it with the real data once that is available. - * - * Accessing nested functions in a LoadingStoreValue yields another LoadingStoreValue: - * new LoadingStoreValue(...).author().organization() // gives another LoadingStoreValue - * - * Using a LoadingStoreValue or a property of a LoadingStoreValue in a view renders to empty strings: - * let user = new LoadingStoreValue(...) - * 'The "' + user + '" is called "' + user.name + '"' // gives 'The "" is called ""' - * - * @param entityLoaded a Promise that resolves to a StoreValue when the entity has finished - * loading from the API - * @param absoluteSelf optional fully qualified URI of the entity being loaded, if available. If passed, the - * returned LoadingStoreValue will return it in calls to .self and ._meta.self - */ -class LoadingStoreValue { - constructor (entityLoaded, absoluteSelf = null) { - const handler = { - get: function (target, prop, _) { - if (prop === Symbol.toPrimitive) { - return () => '' - } - if (['then', 'toJSON', Symbol.toStringTag, 'state', 'getters', '$options', '_isVue', '__file', 'render', 'constructor'].includes(prop)) { - // This is necessary so that Vue's reactivity system understands to treat this LoadingStoreValue - // like a normal object. - return undefined - } - if (prop === 'loading') { - return true - } - if (prop === 'load') { - return entityLoaded - } - if (prop === 'self') { - return absoluteSelf - } - if (prop === '_meta') { - // When _meta is requested on a LoadingStoreValue, we keep on using the unmodified promise, because - // ._meta.load is supposed to resolve to the whole object, not just the ._meta part of it - return new LoadingStoreValue(entityLoaded, absoluteSelf) - } - if (['$reload'].includes(prop)) { - // Skip reloading entities that are already loading - return () => entityLoaded - } - if (['$loadItems', '$post', '$patch', '$del'].includes(prop)) { - // It is important to call entity[prop] without first saving it into a variable, because saving to a - // variable would change the value of `this` inside the function - return (...args) => entityLoaded.then(entity => entity[prop](...args)) - } - const propertyLoaded = entityLoaded.then(entity => entity[prop]) - if (['items', 'allItems'].includes(prop)) { - return new LoadingStoreCollection(propertyLoaded) - } - // Normal property access: return a function that yields another LoadingStoreValue and renders as empty string - const result = templateParams => new LoadingStoreValue(propertyLoaded.then(property => property(templateParams)._meta.load)) - result.loading = true - result.toString = () => '' - return result - } - } - return new Proxy(this, handler) - } -} - -export default LoadingStoreValue diff --git a/src/LoadingStoreValue.ts b/src/LoadingStoreValue.ts new file mode 100644 index 00000000..2a3b3b33 --- /dev/null +++ b/src/LoadingStoreValue.ts @@ -0,0 +1,98 @@ +import LoadingStoreCollection from './LoadingStoreCollection' +import Resource from './interfaces/Resource' +import { QueryablePromise, wrapPromise } from './QueryablePromise' + +/** + * Creates a placeholder for an entity which has not yet finished loading from the API. + * Such a LoadingStoreValue can safely be used in Vue components, since it will render as an empty + * string and Vue's reactivity system will replace it with the real data once that is available. + * + * Accessing nested functions in a LoadingStoreValue yields another LoadingStoreValue: + * new LoadingStoreValue(...).author().organization() // gives another LoadingStoreValue + * + * Using a LoadingStoreValue or a property of a LoadingStoreValue in a view renders to empty strings: + * let user = new LoadingStoreValue(...) + * 'The "' + user + '" is called "' + user.name + '"' // gives 'The "" is called ""' + * + * @param entityLoaded a Promise that resolves to a StoreValue when the entity has finished + * loading from the API + * @param absoluteSelf optional fully qualified URI of the entity being loaded, if available. If passed, the + * returned LoadingStoreValue will return it in calls to .self and ._meta.self + */ +class LoadingStoreValue implements Resource { + public _meta: { + self: string | null, + load: QueryablePromise + loading: boolean + } + + private loadPromise: QueryablePromise + + constructor (entityLoaded: QueryablePromise, absoluteSelf: string | null = null) { + this._meta = { + self: absoluteSelf, + load: entityLoaded, + loading: true + } + + this.loadPromise = entityLoaded + + const handler = { + get: function (target: LoadingStoreValue, prop: string | number | symbol) { + // TODO docu: Why is this neede? + if (prop === Symbol.toPrimitive) { + return () => '' + } + + // This is necessary so that Vue's reactivity system understands to treat this LoadingStoreValue + // like a normal object. + if (['then', 'toJSON', Symbol.toStringTag, 'state', 'getters', '$options', '_isVue', '__file', 'render', 'constructor'].includes(prop as string)) { + return undefined + } + + // proxy for collection items + const propertyLoaded = entityLoaded.then(entity => entity[prop]).catch(() => {}) // eslint-disable-line @typescript-eslint/no-empty-function + if (['items', 'allItems'].includes(prop as string)) { + return new LoadingStoreCollection(propertyLoaded) + } + + // proxy to properties that actually exist on LoadingStoreValue (_meta, $reload, etc.) + if (Reflect.has(target, prop)) { + return Reflect.get(target, prop) + } + + // Proxy to all other unknown properties: return a function that yields another LoadingStoreValue and renders as empty string + const result = templateParams => new LoadingStoreValue(wrapPromise(propertyLoaded.then(property => property(templateParams)._meta.load))) + return result + } + } + return new Proxy(this, handler) + } + + public toString (): string { + return '' + } + + public $reload (): QueryablePromise { + // Skip reloading entities that are already loading + return this.loadPromise + } + + public $loadItems (): QueryablePromise { + return this.loadPromise + } + + public $post (data: unknown): QueryablePromise { + return wrapPromise(this.loadPromise.then(resource => resource.$post(data))) + } + + public $patch (data: unknown): QueryablePromise { + return wrapPromise(this.loadPromise.then(resource => resource.$patch(data))) + } + + public $del (): QueryablePromise { + return wrapPromise(this.loadPromise.then(resource => resource.$del())) + } +} + +export default LoadingStoreValue diff --git a/src/StoreValue.ts b/src/StoreValue.ts index 144811cf..6fbaa326 100644 --- a/src/StoreValue.ts +++ b/src/StoreValue.ts @@ -7,7 +7,7 @@ import Resource from './interfaces/Resource' import ApiActions from './interfaces/ApiActions' import StoreData from './interfaces/StoreData' import StoreValueCreator from './StoreValueCreator.js' -import { InternalConfig } from './interfaces/Config.js' +import { InternalConfig } from './interfaces/Config' /** * Creates an actual StoreValue, by wrapping the given Vuex store storeData. The storeData must not be loading. @@ -19,6 +19,7 @@ class StoreValue extends CanHaveItems implements Resource { public _meta: { self: string, load: QueryablePromise + loading: boolean } private storeData: StoreData diff --git a/src/StoreValueCreator.js b/src/StoreValueCreator.ts similarity index 70% rename from src/StoreValueCreator.js rename to src/StoreValueCreator.ts index a1a8f41c..b5e02038 100644 --- a/src/StoreValueCreator.js +++ b/src/StoreValueCreator.ts @@ -1,8 +1,16 @@ -import StoreValue from './StoreValue.ts' -import LoadingStoreValue from './LoadingStoreValue.js' +import StoreValue from './StoreValue' +import LoadingStoreValue from './LoadingStoreValue' +import ApiActions from './interfaces/ApiActions' +import { InternalConfig } from './interfaces/Config' +import StoreData from './interfaces/StoreData' +import Resource from './interfaces/Resource' +import { wrapPromise } from './QueryablePromise' class StoreValueCreator { - constructor ({ get, reload, post, patch, del, isUnknown }, config = {}) { + private config: InternalConfig + private apiActions: ApiActions + + constructor ({ get, reload, post, patch, del, isUnknown }: ApiActions, config: InternalConfig = {}) { this.apiActions = { get, reload, post, patch, del, isUnknown } this.config = config } @@ -34,11 +42,11 @@ class StoreValueCreator { * @param data entity data from the Vuex store * @returns object wrapped entity ready for use in a frontend component */ - wrap (data) { + wrap (data: StoreData): Resource { const meta = data._meta || { load: Promise.resolve() } if (meta.loading) { - const entityLoaded = meta.load.then(loadedData => new StoreValue(loadedData, this.apiActions, this, this.config)) + const entityLoaded = wrapPromise(meta.load.then(loadedData => new StoreValue(loadedData, this.apiActions, this, this.config))) return new LoadingStoreValue(entityLoaded, this.config.apiRoot + meta.self) } diff --git a/src/interfaces/Resource.ts b/src/interfaces/Resource.ts index a285434c..665cd1eb 100644 --- a/src/interfaces/Resource.ts +++ b/src/interfaces/Resource.ts @@ -2,8 +2,9 @@ import QueryablePromise from '../QueryablePromise' interface Resource { _meta: { - self: string + self: string | null load: QueryablePromise + loading: boolean } $reload: () => QueryablePromise diff --git a/src/interfaces/StoreData.ts b/src/interfaces/StoreData.ts index a34bf7a0..63a71b08 100644 --- a/src/interfaces/StoreData.ts +++ b/src/interfaces/StoreData.ts @@ -1,10 +1,10 @@ -import Resource from './Resource' import QueryablePromise from '../QueryablePromise' interface StoreData { _meta: { self: string - load: QueryablePromise + load: QueryablePromise + loading: boolean } } diff --git a/tests/store.spec.js b/tests/store.spec.js index 4540fc1f..62468325 100644 --- a/tests/store.spec.js +++ b/tests/store.spec.js @@ -289,7 +289,8 @@ describe('API store', () => { const meta = vm.api.get(loadingObject)._meta // then - expect(`${meta}`).toEqual('') + expect(meta.loading).toEqual(true) + expect(meta.self).toEqual(null) done() }) @@ -1207,7 +1208,7 @@ describe('API store', () => { // given axiosMock.onGet('http://localhost/camps/1').reply(200, embeddedSingleEntity.serverResponse) const loadingStoreValue = vm.api.get('/camps/1') - expect(loadingStoreValue.loading).toBe(true) + expect(loadingStoreValue._meta.loading).toBe(true) done() }) From a34b47425c93466ba79cb866cfab0fa26609dab4 Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Sat, 5 Dec 2020 18:02:28 +0100 Subject: [PATCH 007/120] items/allItems as getters --- package.json | 2 +- src/LoadingStoreValue.ts | 35 +++++++++++++++++++++-------------- src/interfaces/Resource.ts | 1 + 3 files changed, 23 insertions(+), 15 deletions(-) diff --git a/package.json b/package.json index 59542325..90387fae 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "clean": "cross-env rimraf dist coverage lib", "coverage": "cross-env npm run build && jest --coverage", "lint": "cross-env eslint src --ext .js,.ts", - "test": "cross-env npm run lint && npm run build && jest", + "test": "jest", "test:debug": "node --inspect-brk ./node_modules/.bin/jest -i" }, "engines": { diff --git a/src/LoadingStoreValue.ts b/src/LoadingStoreValue.ts index 2a3b3b33..0fc2b979 100644 --- a/src/LoadingStoreValue.ts +++ b/src/LoadingStoreValue.ts @@ -26,7 +26,7 @@ class LoadingStoreValue implements Resource { loading: boolean } - private loadPromise: QueryablePromise + private loadResourceSafely: Promise constructor (entityLoaded: QueryablePromise, absoluteSelf: string | null = null) { this._meta = { @@ -35,7 +35,9 @@ class LoadingStoreValue implements Resource { loading: true } - this.loadPromise = entityLoaded + // safe load: if API call fails, suppress errors and present this Proxy again to the chain + const loadResourceSafely = entityLoaded.catch(() => this) + this.loadResourceSafely = loadResourceSafely const handler = { get: function (target: LoadingStoreValue, prop: string | number | symbol) { @@ -50,19 +52,14 @@ class LoadingStoreValue implements Resource { return undefined } - // proxy for collection items - const propertyLoaded = entityLoaded.then(entity => entity[prop]).catch(() => {}) // eslint-disable-line @typescript-eslint/no-empty-function - if (['items', 'allItems'].includes(prop as string)) { - return new LoadingStoreCollection(propertyLoaded) - } - // proxy to properties that actually exist on LoadingStoreValue (_meta, $reload, etc.) if (Reflect.has(target, prop)) { return Reflect.get(target, prop) } // Proxy to all other unknown properties: return a function that yields another LoadingStoreValue and renders as empty string - const result = templateParams => new LoadingStoreValue(wrapPromise(propertyLoaded.then(property => property(templateParams)._meta.load))) + const loadProperty = loadResourceSafely.then(resource => resource[prop]) + const result = templateParams => new LoadingStoreValue(wrapPromise(loadProperty.then(property => property(templateParams)._meta.load))) return result } } @@ -73,25 +70,35 @@ class LoadingStoreValue implements Resource { return '' } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + get items (): any { + return new LoadingStoreCollection(this.loadResourceSafely.then(entity => entity.items)) + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + get allItems (): any { + return new LoadingStoreCollection(this.loadResourceSafely.then(entity => entity.allItems)) + } + public $reload (): QueryablePromise { // Skip reloading entities that are already loading - return this.loadPromise + return this._meta.load } public $loadItems (): QueryablePromise { - return this.loadPromise + return this._meta.load } public $post (data: unknown): QueryablePromise { - return wrapPromise(this.loadPromise.then(resource => resource.$post(data))) + return wrapPromise(this._meta.load.then(resource => resource.$post(data))) } public $patch (data: unknown): QueryablePromise { - return wrapPromise(this.loadPromise.then(resource => resource.$patch(data))) + return wrapPromise(this._meta.load.then(resource => resource.$patch(data))) } public $del (): QueryablePromise { - return wrapPromise(this.loadPromise.then(resource => resource.$del())) + return wrapPromise(this._meta.load.then(resource => resource.$del())) } } diff --git a/src/interfaces/Resource.ts b/src/interfaces/Resource.ts index 665cd1eb..93e2f685 100644 --- a/src/interfaces/Resource.ts +++ b/src/interfaces/Resource.ts @@ -14,6 +14,7 @@ interface Resource { $del: () => QueryablePromise items?: Array + allItems?: Array } export default Resource From 9ae112b405d25b1801a358738d86f12390cf1856 Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Sat, 5 Dec 2020 18:54:57 +0100 Subject: [PATCH 008/120] convert LoadingStoreCollection to TypeScript --- src/CanHaveItems.js | 4 ++-- src/EmbeddedCollection.js | 1 + src/LoadingStoreCollection.js | 31 --------------------------- src/LoadingStoreCollection.ts | 40 +++++++++++++++++++++++++++++++++++ src/LoadingStoreValue.ts | 28 ++++++++++++------------ src/StoreValue.ts | 10 ++++----- src/interfaces/ApiActions.ts | 9 ++++---- src/interfaces/Resource.ts | 10 ++++----- 8 files changed, 70 insertions(+), 63 deletions(-) delete mode 100644 src/LoadingStoreCollection.js create mode 100644 src/LoadingStoreCollection.ts diff --git a/src/CanHaveItems.js b/src/CanHaveItems.js index 448a579c..5531e163 100644 --- a/src/CanHaveItems.js +++ b/src/CanHaveItems.js @@ -43,7 +43,7 @@ class CanHaveItems { if (this.config.avoidNPlusOneRequests) { const completelyLoaded = this.apiActions.reload({ _meta: { reload: { uri: fetchAllUri, property: fetchAllProperty } } }, true) .then(() => this.replaceEntityReferences(array)) - return new LoadingStoreCollection(completelyLoaded) + return LoadingStoreCollection.create(completelyLoaded) } else { const arrayWithReplacedReferences = this.replaceEntityReferences(array) const arrayCompletelyLoaded = Promise.all(array.map(entry => { @@ -52,7 +52,7 @@ class CanHaveItems { } return Promise.resolve(entry) })) - return new LoadingStoreCollection(arrayCompletelyLoaded, arrayWithReplacedReferences) + return LoadingStoreCollection.create(arrayCompletelyLoaded, arrayWithReplacedReferences) } } diff --git a/src/EmbeddedCollection.js b/src/EmbeddedCollection.js index 50dd2c8c..9bcad75f 100644 --- a/src/EmbeddedCollection.js +++ b/src/EmbeddedCollection.js @@ -27,6 +27,7 @@ class EmbeddedCollection extends CanHaveItems { $loadItems () { return new Promise((resolve) => { const items = this.items + // TODO: this is probably broken as LoadingStoreCollection has no constructor anymore if (items instanceof LoadingStoreCollection) items._meta.load.then(result => resolve(result)) else resolve(items) }) diff --git a/src/LoadingStoreCollection.js b/src/LoadingStoreCollection.js deleted file mode 100644 index beb661fd..00000000 --- a/src/LoadingStoreCollection.js +++ /dev/null @@ -1,31 +0,0 @@ -import LoadingStoreValue from './LoadingStoreValue' - -/** - * Returns a placeholder for an array that has not yet finished loading from the API. The array placeholder - * will respond to functional calls (like .find(), .map(), etc.) with further LoadingStoreCollections or - * LoadingStoreValues. If passed the existingContent argument, random access and .length will also work. - * @param arrayLoaded Promise that resolves once the array has finished loading - * @param existingContent optionally set the elements that are already known, for random access - */ -class LoadingStoreCollection { - constructor (arrayLoaded, existingContent = []) { - const singleResultFunctions = ['find'] - const arrayResultFunctions = ['map', 'flatMap', 'filter'] - this._meta = { load: arrayLoaded } - singleResultFunctions.forEach(func => { - existingContent[func] = (...args) => { - const resultLoaded = arrayLoaded.then(array => array[func](...args)) - return new LoadingStoreValue(resultLoaded) - } - }) - arrayResultFunctions.forEach(func => { - existingContent[func] = (...args) => { - const resultLoaded = arrayLoaded.then(array => array[func](...args)) - return new LoadingStoreCollection(resultLoaded) - } - }) - return existingContent - } -} - -export default LoadingStoreCollection diff --git a/src/LoadingStoreCollection.ts b/src/LoadingStoreCollection.ts new file mode 100644 index 00000000..0b7695c2 --- /dev/null +++ b/src/LoadingStoreCollection.ts @@ -0,0 +1,40 @@ +import LoadingStoreValue from './LoadingStoreValue' +import Resource from './interfaces/Resource' + +/** + * Returns a placeholder for an array that has not yet finished loading from the API. The array placeholder + * will respond to functional calls (like .find(), .map(), etc.) with further LoadingStoreCollections or + * LoadingStoreValues. If passed the existingContent argument, random access and .length will also work. + * @param arrayLoaded Promise that resolves once the array has finished loading + * @param existingContent optionally set the elements that are already known, for random access + */ +class LoadingStoreCollection { + static create (loadArray: Promise | undefined>, existingContent: Array = []): Array { + // if Promsise resolves to undefined, provide empty array + // this could happen if items is accessed from a LoadingStoreValue, which resolves to a normal entity without 'items' + const loadArraySafely = loadArray.then(array => array ?? []) + + // proxy array function 'find' to a LadingStoreValue (Resource) + const singleResultFunctions = ['find'] + singleResultFunctions.forEach(func => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + existingContent[func] = (...args: any[]) => { + const resultLoaded = loadArraySafely.then(array => array[func](...args) as Resource) + return new LoadingStoreValue(resultLoaded) + } + }) + + // proxy array functions with multiple results to a LadingStoreCollection (Array) + const arrayResultFunctions = ['map', 'flatMap', 'filter'] + arrayResultFunctions.forEach(func => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + existingContent[func] = (...args: any[]) => { + const resultLoaded = loadArraySafely.then(array => array[func](...args) as Array) + return LoadingStoreCollection.create(resultLoaded) + } + }) + return existingContent + } +} + +export default LoadingStoreCollection diff --git a/src/LoadingStoreValue.ts b/src/LoadingStoreValue.ts index 0fc2b979..69fefedd 100644 --- a/src/LoadingStoreValue.ts +++ b/src/LoadingStoreValue.ts @@ -28,10 +28,10 @@ class LoadingStoreValue implements Resource { private loadResourceSafely: Promise - constructor (entityLoaded: QueryablePromise, absoluteSelf: string | null = null) { + constructor (entityLoaded: Promise, absoluteSelf: string | null = null) { this._meta = { self: absoluteSelf, - load: entityLoaded, + load: wrapPromise(entityLoaded), loading: true } @@ -57,9 +57,9 @@ class LoadingStoreValue implements Resource { return Reflect.get(target, prop) } - // Proxy to all other unknown properties: return a function that yields another LoadingStoreValue and renders as empty string + // Proxy to all other unknown properties: return a function that yields another LoadingStoreValue const loadProperty = loadResourceSafely.then(resource => resource[prop]) - const result = templateParams => new LoadingStoreValue(wrapPromise(loadProperty.then(property => property(templateParams)._meta.load))) + const result = templateParams => new LoadingStoreValue(loadProperty.then(property => property(templateParams)._meta.load)) return result } } @@ -70,34 +70,32 @@ class LoadingStoreValue implements Resource { return '' } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - get items (): any { - return new LoadingStoreCollection(this.loadResourceSafely.then(entity => entity.items)) + get items (): Array { + return LoadingStoreCollection.create(this.loadResourceSafely.then(entity => entity.items)) } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - get allItems (): any { - return new LoadingStoreCollection(this.loadResourceSafely.then(entity => entity.allItems)) + get allItems (): Array { + return LoadingStoreCollection.create(this.loadResourceSafely.then(entity => entity.allItems)) } - public $reload (): QueryablePromise { + public $reload (): Promise { // Skip reloading entities that are already loading return this._meta.load } - public $loadItems (): QueryablePromise { + public $loadItems (): Promise { return this._meta.load } - public $post (data: unknown): QueryablePromise { + public $post (data: unknown):Promise { return wrapPromise(this._meta.load.then(resource => resource.$post(data))) } - public $patch (data: unknown): QueryablePromise { + public $patch (data: unknown): Promise { return wrapPromise(this._meta.load.then(resource => resource.$patch(data))) } - public $del (): QueryablePromise { + public $del (): Promise { return wrapPromise(this._meta.load.then(resource => resource.$del())) } } diff --git a/src/StoreValue.ts b/src/StoreValue.ts index 6fbaa326..278482d8 100644 --- a/src/StoreValue.ts +++ b/src/StoreValue.ts @@ -71,23 +71,23 @@ class StoreValue extends CanHaveItems implements Resource { } } - $reload (): QueryablePromise { + $reload (): Promise { return this.apiActions.reload(this._meta.self) } - $loadItems (): QueryablePromise { + $loadItems (): Promise { return this._meta.load } - $post (data: unknown): QueryablePromise { + $post (data: unknown): Promise { return this.apiActions.post(this._meta.self, data) } - $patch (data: unknown): QueryablePromise { + $patch (data: unknown): Promise { return this.apiActions.patch(this._meta.self, data) } - $del (): QueryablePromise { + $del (): Promise { return this.apiActions.del(this._meta.self) } } diff --git a/src/interfaces/ApiActions.ts b/src/interfaces/ApiActions.ts index 214ae08f..38a4d1e4 100644 --- a/src/interfaces/ApiActions.ts +++ b/src/interfaces/ApiActions.ts @@ -1,12 +1,11 @@ import Resource from './Resource' -import QueryablePromise from '../QueryablePromise' interface ApiActions { get: (uriOrEntity: string | Resource, forceReload?: boolean) => Resource - reload: (uriOrEntity: string | Resource) => QueryablePromise - post: (uriOrEntity: string | Resource, data: unknown) => QueryablePromise - patch: (uriOrEntity: string | Resource, data: unknown) => QueryablePromise - del: (uriOrEntity: string | Resource) => QueryablePromise + reload: (uriOrEntity: string | Resource) => Promise + post: (uriOrEntity: string | Resource, data: unknown) =>Promise + patch: (uriOrEntity: string | Resource, data: unknown) => Promise + del: (uriOrEntity: string | Resource) => Promise isUnknown: (uri: string) => boolean } diff --git a/src/interfaces/Resource.ts b/src/interfaces/Resource.ts index 93e2f685..54d0ff7c 100644 --- a/src/interfaces/Resource.ts +++ b/src/interfaces/Resource.ts @@ -7,11 +7,11 @@ interface Resource { loading: boolean } - $reload: () => QueryablePromise - $loadItems: () => QueryablePromise - $post: (data: unknown) => QueryablePromise - $patch: (data: unknown) => QueryablePromise - $del: () => QueryablePromise + $reload: () => Promise + $loadItems: () => Promise + $post: (data: unknown) => Promise + $patch: (data: unknown) => Promise + $del: () => Promise items?: Array allItems?: Array From 134858895cc6d39defece50a0ea861db1e8e5a0e Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Sun, 6 Dec 2020 08:22:24 +0100 Subject: [PATCH 009/120] convert EmbeddedCollection and CanHaveItems to Typescript --- src/{CanHaveItems.js => CanHaveItems.ts} | 55 ++++++++++++++++++------ src/EmbeddedCollection.js | 37 ---------------- src/EmbeddedCollection.ts | 55 ++++++++++++++++++++++++ src/LoadingStoreCollection.ts | 14 +++--- src/StoreValue.ts | 18 ++++---- src/{halHelpers.js => halHelpers.ts} | 13 ++++-- src/interfaces/ApiActions.ts | 6 +-- src/interfaces/Resource.ts | 19 ++++++++ src/interfaces/StoreData.ts | 16 ++++++- 9 files changed, 160 insertions(+), 73 deletions(-) rename src/{CanHaveItems.js => CanHaveItems.ts} (59%) delete mode 100644 src/EmbeddedCollection.js create mode 100644 src/EmbeddedCollection.ts rename src/{halHelpers.js => halHelpers.ts} (71%) diff --git a/src/CanHaveItems.js b/src/CanHaveItems.ts similarity index 59% rename from src/CanHaveItems.js rename to src/CanHaveItems.ts index 5531e163..d0003171 100644 --- a/src/CanHaveItems.js +++ b/src/CanHaveItems.ts @@ -1,9 +1,26 @@ -import { isEntityReference } from './halHelpers.js' +import { isEntityReference } from './halHelpers' import LoadingStoreCollection from './LoadingStoreCollection' +import Resource from './interfaces/Resource' +import ApiActions from './interfaces/ApiActions' +import { InternalConfig } from './interfaces/Config' +import { Link } from './interfaces/StoreData' -class CanHaveItems { - constructor ({ get, reload, isUnknown }, config) { - this.apiActions = { get, reload, isUnknown } +interface Collection { + items: Array + allItems: Array +} + +// type keyValueObject = Record + +class CanHaveItems implements Collection { + apiActions: ApiActions + config: InternalConfig + + items: Array = [] + allItems: Array = [] + + constructor (apiActions: ApiActions, config: InternalConfig) { + this.apiActions = apiActions this.config = config } @@ -16,12 +33,15 @@ class CanHaveItems { * @param property property name inside the entity fetched at fetchAllUri that contains the collection * @returns object the target object with the added getter */ - addItemsGetter (items, fetchAllUri, property) { + addItemsGetter (items: Array, fetchAllUri: string, property: string): void { Object.defineProperty(this, 'items', { get: () => this.filterDeleting(this.mapArrayOfEntityReferences(items, fetchAllUri, property)) }) Object.defineProperty(this, 'allItems', { get: () => this.mapArrayOfEntityReferences(items, fetchAllUri, property) }) } - filterDeleting (array) { + /** + * Filter out items that are mareked as deleting (eager removal) + */ + filterDeleting (array: Array): Array { return array.filter(entry => !entry._meta.deleting) } @@ -35,20 +55,25 @@ class CanHaveItems { * @returns array the new array with replaced items, or a LoadingStoreCollection if any of the array * elements is still loading. */ - mapArrayOfEntityReferences (array, fetchAllUri, fetchAllProperty) { + mapArrayOfEntityReferences (array: Array, fetchAllUri: string, fetchAllProperty: string): Array { if (!this.containsUnknownEntityReference(array)) { return this.replaceEntityReferences(array) } + // eager loading of 'fetchAllUri' (e.g. parent for embedded collections) if (this.config.avoidNPlusOneRequests) { - const completelyLoaded = this.apiActions.reload({ _meta: { reload: { uri: fetchAllUri, property: fetchAllProperty } } }, true) + const completelyLoaded = this.apiActions.reload({ _meta: { reload: { uri: fetchAllUri, property: fetchAllProperty } } }) .then(() => this.replaceEntityReferences(array)) return LoadingStoreCollection.create(completelyLoaded) + + // no eager loading: replace each reference (Link) with a StoreValue (Resource) } else { const arrayWithReplacedReferences = this.replaceEntityReferences(array) + + // TODO: why is the next step needed? Is it not sufficient to only return arrayWithReplacedReferences? const arrayCompletelyLoaded = Promise.all(array.map(entry => { if (isEntityReference(entry)) { - return this.apiActions.get(entry.href)._meta.load + return this.apiActions.get(entry.href)._meta.load // also TODO: we generate a StoreValue for each entry again, which was already done above with replaceEntityReferences } return Promise.resolve(entry) })) @@ -56,16 +81,22 @@ class CanHaveItems { } } - replaceEntityReferences (array) { + /** + * Replace each item in array with a proper StoreValue (or LoadingStoreValue) + */ + replaceEntityReferences (array: Array): Array { return array.map(entry => { if (isEntityReference(entry)) { return this.apiActions.get(entry.href) } - return entry + return entry as Resource // TODO: in which case would this happen? shouldn't 'items' always contain entity references }) } - containsUnknownEntityReference (array) { + /** + * Returns true if any of the items within 'array' is not yet known to the API (=has never been loaded) + */ + containsUnknownEntityReference (array: Array): boolean { return array.some(entry => isEntityReference(entry) && this.apiActions.isUnknown(entry.href)) } } diff --git a/src/EmbeddedCollection.js b/src/EmbeddedCollection.js deleted file mode 100644 index 9bcad75f..00000000 --- a/src/EmbeddedCollection.js +++ /dev/null @@ -1,37 +0,0 @@ -import CanHaveItems from './CanHaveItems.js' -import LoadingStoreCollection from './LoadingStoreCollection' - -/** - * Imitates a full standalone collection with an items property, even if there is no separate URI (as it - * is the case with embedded collections). - * Reloading an embedded collection requires special information. Since the embedded collection has no own - * URI, we need to reload the whole entity containing the embedded collection. Some extra info about the - * containing entity must therefore be passed to this function. - * @param items array of items, which can be mixed primitive values and entity references - * @param reloadUri URI of the entity containing the embedded collection (for reloading) - * @param reloadProperty property in the containing entity under which the embedded collection is saved - * @param loadPromise a promise that will resolve when the parent entity has finished (re-)loading - */ -class EmbeddedCollection extends CanHaveItems { - constructor (items, reloadUri, reloadProperty, { get, reload, isUnknown }, config, loadPromise = null) { - super({ get, reload, isUnknown }, config) - this._meta = { - load: loadPromise - ? loadPromise.then(loadedParent => new EmbeddedCollection(loadedParent[reloadProperty], reloadUri, reloadProperty, { get, reload, isUnknown }, config)) - : Promise.resolve(this), - reload: { uri: reloadUri, property: reloadProperty } - } - this.addItemsGetter(items, reloadUri, reloadProperty) - } - - $loadItems () { - return new Promise((resolve) => { - const items = this.items - // TODO: this is probably broken as LoadingStoreCollection has no constructor anymore - if (items instanceof LoadingStoreCollection) items._meta.load.then(result => resolve(result)) - else resolve(items) - }) - } -} - -export default EmbeddedCollection diff --git a/src/EmbeddedCollection.ts b/src/EmbeddedCollection.ts new file mode 100644 index 00000000..2e08f171 --- /dev/null +++ b/src/EmbeddedCollection.ts @@ -0,0 +1,55 @@ +import CanHaveItems from './CanHaveItems' +// import LoadingStoreCollection from './LoadingStoreCollection' +import Resource, { EmbeddedCollectionType } from './interfaces/Resource' +import ApiActions from './interfaces/ApiActions' +import { InternalConfig } from './interfaces/Config' +import StoreData, { Link } from './interfaces/StoreData' + +/** + * Imitates a full standalone collection with an items property, even if there is no separate URI (as it + * is the case with embedded collections). + * Reloading an embedded collection requires special information. Since the embedded collection has no own + * URI, we need to reload the whole entity containing the embedded collection. Some extra info about the + * containing entity must therefore be passed to this function. + * @param items array of items, which can be mixed primitive values and entity references + * @param reloadUri URI of the entity containing the embedded collection (for reloading) + * @param reloadProperty property in the containing entity under which the embedded collection is saved + * @param loadPromise a promise that will resolve when the parent entity has finished (re-)loading + */ +class EmbeddedCollection extends CanHaveItems implements EmbeddedCollectionType { + // TODO: do we want an interfae for this + // TODOL do we want to expose the Resource interface here, such that embedded collections have the same public API indepdendent whether a collection is embedded or not + public _meta: { + load: Promise, + reload: { // TODO: do we want/need to expose this eternally? or sufficient if we keep this in the store and expose $reload()? + uri: string, + property: string + } + } + + constructor (items: Array, reloadUri: string, reloadProperty: string, apiActions: ApiActions, config: InternalConfig, loadParent: Promise | null = null) { + super(apiActions, config) + this._meta = { + load: loadParent + ? loadParent.then(parentResource => new EmbeddedCollection(parentResource[reloadProperty], reloadUri, reloadProperty, apiActions, config)) + : Promise.resolve(this), + reload: { + uri: reloadUri, + property: reloadProperty + } + } + this.addItemsGetter(items, reloadUri, reloadProperty) + } + + $loadItems () :Promise> { + return new Promise((resolve) => { + const items = this.items + // TODO: this is probably broken as LoadingStoreCollection has no constructor anymore + // if (items instanceof LoadingStoreCollection) items._meta.load.then(result => resolve(result)) + // else resolve(items) + resolve(items) + }) + } +} + +export default EmbeddedCollection diff --git a/src/LoadingStoreCollection.ts b/src/LoadingStoreCollection.ts index 0b7695c2..129b5aa1 100644 --- a/src/LoadingStoreCollection.ts +++ b/src/LoadingStoreCollection.ts @@ -1,14 +1,14 @@ import LoadingStoreValue from './LoadingStoreValue' import Resource from './interfaces/Resource' -/** - * Returns a placeholder for an array that has not yet finished loading from the API. The array placeholder - * will respond to functional calls (like .find(), .map(), etc.) with further LoadingStoreCollections or - * LoadingStoreValues. If passed the existingContent argument, random access and .length will also work. - * @param arrayLoaded Promise that resolves once the array has finished loading - * @param existingContent optionally set the elements that are already known, for random access - */ class LoadingStoreCollection { + /** + * Returns a placeholder for an array that has not yet finished loading from the API. The array placeholder + * will respond to functional calls (like .find(), .map(), etc.) with further LoadingStoreCollections or + * LoadingStoreValues. If passed the existingContent argument, random access and .length will also work. + * @param arrayLoaded Promise that resolves once the array has finished loading + * @param existingContent optionally set the elements that are already known, for random access + */ static create (loadArray: Promise | undefined>, existingContent: Array = []): Array { // if Promsise resolves to undefined, provide empty array // this could happen if items is accessed from a LoadingStoreValue, which resolves to a normal entity without 'items' diff --git a/src/StoreValue.ts b/src/StoreValue.ts index 278482d8..e32f0448 100644 --- a/src/StoreValue.ts +++ b/src/StoreValue.ts @@ -1,12 +1,12 @@ import urltemplate from 'url-template' -import { isTemplatedLink, isEntityReference, isCollection } from './halHelpers.js' +import { isTemplatedLink, isEntityReference, isCollection } from './halHelpers' import { QueryablePromise, createResolvedPromise, wrapPromise } from './QueryablePromise' -import EmbeddedCollection from './EmbeddedCollection.js' -import CanHaveItems from './CanHaveItems.js' +import EmbeddedCollection from './EmbeddedCollection' +import CanHaveItems from './CanHaveItems' import Resource from './interfaces/Resource' import ApiActions from './interfaces/ApiActions' import StoreData from './interfaces/StoreData' -import StoreValueCreator from './StoreValueCreator.js' +import StoreValueCreator from './StoreValueCreator' import { InternalConfig } from './interfaces/Config' /** @@ -26,10 +26,10 @@ class StoreValue extends CanHaveItems implements Resource { config: InternalConfig apiActions: ApiActions - constructor (storeData: StoreData, { get, reload, post, patch, del, isUnknown }: ApiActions, storeValueCreator: StoreValueCreator, config: InternalConfig) { - super({ get, reload, isUnknown }, config) + constructor (storeData: StoreData, apiActions: ApiActions, storeValueCreator: StoreValueCreator, config: InternalConfig) { + super(apiActions, config) - this.apiActions = { get, reload, post, patch, del, isUnknown } + this.apiActions = apiActions this.config = config this.storeData = storeData @@ -38,11 +38,11 @@ class StoreValue extends CanHaveItems implements Resource { // storeData is a collection: add keys to retrieve collection items if (key === 'items' && isCollection(storeData)) { - this.addItemsGetter(storeData[key], storeData._meta.self, key) + this.addItemsGetter(storeData.items, storeData._meta.self, key) // storeData[key] is an embedded collection } else if (Array.isArray(value)) { - this[key] = () => new EmbeddedCollection(value, storeData._meta.self, key, { get, reload, isUnknown }, config, storeData._meta.load) + this[key] = () => new EmbeddedCollection(value, storeData._meta.self, key, this.apiActions, config, storeData._meta.load) // storeData[key] is a reference only (contains only href; no data) } else if (isEntityReference(value)) { diff --git a/src/halHelpers.js b/src/halHelpers.ts similarity index 71% rename from src/halHelpers.js rename to src/halHelpers.ts index 6e2c8a7d..16a4ca4c 100644 --- a/src/halHelpers.js +++ b/src/halHelpers.ts @@ -1,7 +1,11 @@ +import { Link, TemplatedLink, Collection } from './interfaces/StoreData' + +type keyValueObject = Record + /** * Verifies that two arrays contain the same values while ignoring the order */ -function isEqualIgnoringOrder (array, other) { +function isEqualIgnoringOrder (array: Array, other: Array) :boolean { return array.length === other.length && array.every(elem => other.includes(elem)) } @@ -10,17 +14,18 @@ function isEqualIgnoringOrder (array, other) { * @param object to be examined * @returns boolean true if the object looks like a templated link, false otherwise */ -function isTemplatedLink (object) { +function isTemplatedLink (object: keyValueObject): object is TemplatedLink { if (!object) return false return isEqualIgnoringOrder(Object.keys(object), ['href', 'templated']) && (object.templated === true) } /** * An entity reference in the Vuex store looks like this: { href: '/some/uri' } + * Serves as a type guard for interface EntityReference * @param object to be examined * @returns boolean true if the object looks like an entity reference, false otherwise */ -function isEntityReference (object) { +function isEntityReference (object: keyValueObject): object is Link { if (!object) return false return isEqualIgnoringOrder(Object.keys(object), ['href']) } @@ -30,7 +35,7 @@ function isEntityReference (object) { * @param object to be examined * @returns boolean true if the object looks like a standalone collection, false otherwise */ -function isCollection (object) { +function isCollection (object: keyValueObject): object is Collection { return !!(object && Array.isArray(object.items)) } diff --git a/src/interfaces/ApiActions.ts b/src/interfaces/ApiActions.ts index 38a4d1e4..56c86416 100644 --- a/src/interfaces/ApiActions.ts +++ b/src/interfaces/ApiActions.ts @@ -1,8 +1,8 @@ -import Resource from './Resource' +import Resource, { EmbeddedCollectionType } from './Resource' interface ApiActions { - get: (uriOrEntity: string | Resource, forceReload?: boolean) => Resource - reload: (uriOrEntity: string | Resource) => Promise + get: (uriOrEntity: string | Resource | EmbeddedCollectionType, forceReload?: boolean) => Resource + reload: (uriOrEntity: string | Resource | EmbeddedCollectionType) => Promise post: (uriOrEntity: string | Resource, data: unknown) =>Promise patch: (uriOrEntity: string | Resource, data: unknown) => Promise del: (uriOrEntity: string | Resource) => Promise diff --git a/src/interfaces/Resource.ts b/src/interfaces/Resource.ts index 54d0ff7c..cb17859b 100644 --- a/src/interfaces/Resource.ts +++ b/src/interfaces/Resource.ts @@ -1,10 +1,15 @@ import QueryablePromise from '../QueryablePromise' +/** + * Generic interface for a standalone Resource (e.g. a HAl resource with an own store entry and a self link) + * Can be a collection or a single entity + */ interface Resource { _meta: { self: string | null load: QueryablePromise loading: boolean + deleting?: boolean } $reload: () => Promise @@ -17,4 +22,18 @@ interface Resource { allItems?: Array } +/** + * Subtype for an embeddeed collection with no self link (no standalone store entry, exists only with its parent) + */ +type EmbeddedCollectionType = { + _meta: { + load?: Promise + reload: { + uri: string + property: string + } + } +} + +export { Resource, EmbeddedCollectionType } export default Resource diff --git a/src/interfaces/StoreData.ts b/src/interfaces/StoreData.ts index 63a71b08..b90a11ff 100644 --- a/src/interfaces/StoreData.ts +++ b/src/interfaces/StoreData.ts @@ -1,6 +1,6 @@ import QueryablePromise from '../QueryablePromise' -interface StoreData { +type StoreData = { _meta: { self: string load: QueryablePromise @@ -8,4 +8,18 @@ interface StoreData { } } +type Link = { + href: string +} + +type TemplatedLink = Link & { + templated: string +} + +type Collection = { + items: Array +} + +export { StoreData, Link, TemplatedLink, Collection } + export default StoreData From 9477b058c325dd8a5defebf66f288c287dba5bb5 Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Sun, 6 Dec 2020 08:54:46 +0100 Subject: [PATCH 010/120] items and allItems as normal getters --- src/CanHaveItems.ts | 49 +++++++++++++++++++++++---------------- src/EmbeddedCollection.ts | 8 +++---- src/StoreValue.ts | 44 ++++++++++++++++++----------------- 3 files changed, 55 insertions(+), 46 deletions(-) diff --git a/src/CanHaveItems.ts b/src/CanHaveItems.ts index d0003171..1c2c2bb2 100644 --- a/src/CanHaveItems.ts +++ b/src/CanHaveItems.ts @@ -10,38 +10,47 @@ interface Collection { allItems: Array } -// type keyValueObject = Record - class CanHaveItems implements Collection { apiActions: ApiActions config: InternalConfig - items: Array = [] - allItems: Array = [] + private storeItems: Array + private fetchAllUri: string + private fetchAllProperty: string - constructor (apiActions: ApiActions, config: InternalConfig) { + /** + * @param storeItems array of items, which can be mixed primitive values and entity references + * @param fetchAllUri URI that allows fetching all collection items in a single network request, if known + * @param fetchAllProperty property name inside the entity fetched at fetchAllUri that contains the collection + */ + constructor (apiActions: ApiActions, config: InternalConfig, storeItems: Array, fetchAllUri: string, fetchAllProperty: string) { this.apiActions = apiActions this.config = config + this.storeItems = storeItems + this.fetchAllUri = fetchAllUri + this.fetchAllProperty = fetchAllProperty } /** - * Defines a property getter for the items property. - * The items property should always be a getter, in order to make the call to mapArrayOfEntityReferences - * lazy, since that potentially fetches a large number of entities from the API. - * @param items array of items, which can be mixed primitive values and entity references - * @param fetchAllUri URI that allows fetching all collection items in a single network request, if known - * @param property property name inside the entity fetched at fetchAllUri that contains the collection - * @returns object the target object with the added getter - */ - addItemsGetter (items: Array, fetchAllUri: string, property: string): void { - Object.defineProperty(this, 'items', { get: () => this.filterDeleting(this.mapArrayOfEntityReferences(items, fetchAllUri, property)) }) - Object.defineProperty(this, 'allItems', { get: () => this.mapArrayOfEntityReferences(items, fetchAllUri, property) }) + * Get items excluding ones marked as 'deleting' (eager remove) + * The items property should always be a getter, in order to make the call to mapArrayOfEntityReferences + * lazy, since that potentially fetches a large number of entities from the API. + */ + public get items (): Array { + return this.filterDeleting(this.mapArrayOfEntityReferences(this.storeItems, this.fetchAllUri, this.fetchAllProperty)) + } + + /** + * Get all items including ones marked as 'deleting' (lazy remove) + */ + public get allItems (): Array { + return this.mapArrayOfEntityReferences(this.storeItems, this.fetchAllUri, this.fetchAllProperty) } /** * Filter out items that are mareked as deleting (eager removal) */ - filterDeleting (array: Array): Array { + private filterDeleting (array: Array): Array { return array.filter(entry => !entry._meta.deleting) } @@ -55,7 +64,7 @@ class CanHaveItems implements Collection { * @returns array the new array with replaced items, or a LoadingStoreCollection if any of the array * elements is still loading. */ - mapArrayOfEntityReferences (array: Array, fetchAllUri: string, fetchAllProperty: string): Array { + private mapArrayOfEntityReferences (array: Array, fetchAllUri: string, fetchAllProperty: string): Array { if (!this.containsUnknownEntityReference(array)) { return this.replaceEntityReferences(array) } @@ -84,7 +93,7 @@ class CanHaveItems implements Collection { /** * Replace each item in array with a proper StoreValue (or LoadingStoreValue) */ - replaceEntityReferences (array: Array): Array { + private replaceEntityReferences (array: Array): Array { return array.map(entry => { if (isEntityReference(entry)) { return this.apiActions.get(entry.href) @@ -96,7 +105,7 @@ class CanHaveItems implements Collection { /** * Returns true if any of the items within 'array' is not yet known to the API (=has never been loaded) */ - containsUnknownEntityReference (array: Array): boolean { + private containsUnknownEntityReference (array: Array): boolean { return array.some(entry => isEntityReference(entry) && this.apiActions.isUnknown(entry.href)) } } diff --git a/src/EmbeddedCollection.ts b/src/EmbeddedCollection.ts index 2e08f171..7cd60eb8 100644 --- a/src/EmbeddedCollection.ts +++ b/src/EmbeddedCollection.ts @@ -17,18 +17,17 @@ import StoreData, { Link } from './interfaces/StoreData' * @param loadPromise a promise that will resolve when the parent entity has finished (re-)loading */ class EmbeddedCollection extends CanHaveItems implements EmbeddedCollectionType { - // TODO: do we want an interfae for this - // TODOL do we want to expose the Resource interface here, such that embedded collections have the same public API indepdendent whether a collection is embedded or not public _meta: { load: Promise, - reload: { // TODO: do we want/need to expose this eternally? or sufficient if we keep this in the store and expose $reload()? + reload: { // TODO: do we want/need to expose this externally? or sufficient if we keep this in the store and expose $reload()? uri: string, property: string } } constructor (items: Array, reloadUri: string, reloadProperty: string, apiActions: ApiActions, config: InternalConfig, loadParent: Promise | null = null) { - super(apiActions, config) + super(apiActions, config, items, reloadUri, reloadProperty) + this._meta = { load: loadParent ? loadParent.then(parentResource => new EmbeddedCollection(parentResource[reloadProperty], reloadUri, reloadProperty, apiActions, config)) @@ -38,7 +37,6 @@ class EmbeddedCollection extends CanHaveItems implements EmbeddedCollectionType property: reloadProperty } } - this.addItemsGetter(items, reloadUri, reloadProperty) } $loadItems () :Promise> { diff --git a/src/StoreValue.ts b/src/StoreValue.ts index e32f0448..0459b50f 100644 --- a/src/StoreValue.ts +++ b/src/StoreValue.ts @@ -27,36 +27,38 @@ class StoreValue extends CanHaveItems implements Resource { apiActions: ApiActions constructor (storeData: StoreData, apiActions: ApiActions, storeValueCreator: StoreValueCreator, config: InternalConfig) { - super(apiActions, config) + if (isCollection(storeData)) { + super(apiActions, config, storeData.items, storeData._meta.self, 'items') + } else { + super(apiActions, config, [], '', '') // TODO: consider implementing CanHaveItems as mixin, then call super constructor is not necessary for non-collections + } this.apiActions = apiActions this.config = config this.storeData = storeData - Object.keys(storeData).forEach(key => { - const value = storeData[key] - - // storeData is a collection: add keys to retrieve collection items - if (key === 'items' && isCollection(storeData)) { - this.addItemsGetter(storeData.items, storeData._meta.self, key) + Object.keys(storeData) + .filter(key => !['items', '_meta'].includes(key)) // exclude reserved properties + .forEach(key => { + const value = storeData[key] - // storeData[key] is an embedded collection - } else if (Array.isArray(value)) { - this[key] = () => new EmbeddedCollection(value, storeData._meta.self, key, this.apiActions, config, storeData._meta.load) + // storeData[key] is an embedded collection + if (Array.isArray(value)) { + this[key] = () => new EmbeddedCollection(value, storeData._meta.self, key, this.apiActions, config, storeData._meta.load) - // storeData[key] is a reference only (contains only href; no data) - } else if (isEntityReference(value)) { - this[key] = () => this.apiActions.get(value.href) + // storeData[key] is a reference only (contains only href; no data) + } else if (isEntityReference(value)) { + this[key] = () => this.apiActions.get(value.href) - // storeData[key] is a templated link - } else if (isTemplatedLink(value)) { - this[key] = templateParams => this.apiActions.get(urltemplate.parse(value.href).expand(templateParams || {})) + // storeData[key] is a templated link + } else if (isTemplatedLink(value)) { + this[key] = templateParams => this.apiActions.get(urltemplate.parse(value.href).expand(templateParams || {})) - // storeData[key] is a primitive (normal entity property) - } else { - this[key] = value - } - }) + // storeData[key] is a primitive (normal entity property) + } else { + this[key] = value + } + }) // Use a trivial load promise to break endless recursion, except if we are currently reloading the storeData from the API const loadPromise = storeData._meta.load && storeData._meta.load.isPending() From 6a5603697a6fe61edd0704d852bb26a6f1a88fe2 Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Sun, 6 Dec 2020 09:03:13 +0100 Subject: [PATCH 011/120] convert normalizeUri --- src/index.js | 2 +- src/{normalizeUri.js => normalizeEntityUri.ts} | 11 ++++++++--- tests/normalizeUri.spec.js | 2 +- 3 files changed, 10 insertions(+), 5 deletions(-) rename src/{normalizeUri.js => normalizeEntityUri.ts} (84%) diff --git a/src/index.js b/src/index.js index dba304e0..2b02d19a 100644 --- a/src/index.js +++ b/src/index.js @@ -1,6 +1,6 @@ import normalize from 'hal-json-normalizer' import urltemplate from 'url-template' -import { normalizeEntityUri } from './normalizeUri' +import normalizeEntityUri from './normalizeEntityUri' import StoreValueCreator from './StoreValueCreator' import StoreValue from './StoreValue.ts' import LoadingStoreValue from './LoadingStoreValue' diff --git a/src/normalizeUri.js b/src/normalizeEntityUri.ts similarity index 84% rename from src/normalizeUri.js rename to src/normalizeEntityUri.ts index 8260273a..55ea0609 100644 --- a/src/normalizeUri.js +++ b/src/normalizeEntityUri.ts @@ -1,3 +1,6 @@ +import StoreData from './interfaces/StoreData' +import { URLSearchParams } from 'url' + /** * Sorts the query parameters in a URI, keeping the values of duplicate keys in order. * Example: @@ -6,7 +9,7 @@ * @param uri to be processed * @returns string URI with sorted query parameters */ -function sortQueryParams (uri) { +function sortQueryParams (uri: string): string { const queryStart = uri.indexOf('?') if (queryStart === -1) return uri @@ -33,7 +36,7 @@ function sortQueryParams (uri) { * @param baseUrl common URI prefix to remove during normalization * @returns {null|string} normalized URI, or null if the uriOrEntity argument was not understood */ -export function normalizeEntityUri (uriOrEntity, baseUrl = '') { +function normalizeEntityUri (uriOrEntity: string | StoreData, baseUrl = ''): string | null { if (uriOrEntity === undefined) return normalizeUri('', baseUrl) if (typeof uriOrEntity === 'string') return normalizeUri(uriOrEntity, baseUrl) return normalizeUri(((uriOrEntity || {})._meta || {}).self, baseUrl) @@ -45,7 +48,9 @@ export function normalizeEntityUri (uriOrEntity, baseUrl = '') { * @param baseUrl prefix to remove from the beginning of the URI if present * @returns {null|string} normalized URI, or null if uri is not a string */ -function normalizeUri (uri, baseUrl) { +function normalizeUri (uri: unknown, baseUrl: string): string | null { if (typeof uri !== 'string') return null return sortQueryParams(uri).replace(new RegExp(`^${baseUrl}`), '') } + +export default normalizeEntityUri diff --git a/tests/normalizeUri.spec.js b/tests/normalizeUri.spec.js index 661d1cab..702dd292 100644 --- a/tests/normalizeUri.spec.js +++ b/tests/normalizeUri.spec.js @@ -1,4 +1,4 @@ -import { normalizeEntityUri } from '../src/normalizeUri' +import normalizeEntityUri from '../src/normalizeEntityUri' describe('URI normalizing', () => { it('sorts query parameters correctly', () => { From 0707c5f78774822bc9d344272847b5e4fe33a3f0 Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Sun, 6 Dec 2020 09:19:19 +0100 Subject: [PATCH 012/120] convert storeModule --- src/{storeModule.js => storeModule.ts} | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) rename src/{storeModule.js => storeModule.ts} (77%) diff --git a/src/storeModule.js b/src/storeModule.ts similarity index 77% rename from src/storeModule.js rename to src/storeModule.ts index 4282a89d..3b0e9afd 100644 --- a/src/storeModule.js +++ b/src/storeModule.ts @@ -1,15 +1,19 @@ import Vue from 'vue' +import StoreData from './interfaces/StoreData' + +import { MutationTree } from 'vuex/types' export const state = {} +type State = Record -export const mutations = { +export const mutations: MutationTree = { /** * Adds a placeholder into the store that indicates that the entity with the given URI is currently being * fetched from the API and not yet available. * @param state Vuex state * @param uri URI of the object that is being fetched */ - addEmpty (state, uri) { + addEmpty (state: State, uri: string) : void { Vue.set(state, uri, { _meta: { self: uri, loading: true } }) }, /** @@ -17,7 +21,7 @@ export const mutations = { * @param state Vuex state * @param data An object mapping URIs to entities that should be merged into the Vuex state. */ - add (state, data) { + add (state: State, data: Record) : void { Object.keys(data).forEach(uri => { Vue.set(state, uri, data[uri]) }) @@ -27,7 +31,7 @@ export const mutations = { * @param state Vuex state * @param uri URI of the entity that is currently being reloaded */ - reloading (state, uri) { + reloading (state: State, uri: string) : void { if (state[uri]) Vue.set(state[uri]._meta, 'reloading', true) }, /** @@ -35,7 +39,7 @@ export const mutations = { * @param state Vuex state * @param uri URI of the entity that is currently being reloaded */ - reloadingFailed (state, uri) { + reloadingFailed (state: State, uri: string) : void { if (state[uri]) Vue.set(state[uri]._meta, 'reloading', false) }, /** @@ -43,15 +47,15 @@ export const mutations = { * @param state Vuex state * @param uri URI of the entity to be removed */ - purge (state, uri) { + purge (state: State, uri: string) : void { Vue.delete(state, uri) }, /** - * Removes a single entity from the Vuex store. + * Removes all entities from the Vuex store. * @param state Vuex state * @param uri URI of the entity to be removed */ - purgeAll (state, uri) { + purgeAll (state: State) : void { Object.keys(state).forEach(uri => { Vue.delete(state, uri) }) @@ -61,7 +65,7 @@ export const mutations = { * @param state Vuex state * @param uri URI of the entity that is currently being deleted */ - deleting (state, uri) { + deleting (state: State, uri: string) : void { if (state[uri]) Vue.set(state[uri]._meta, 'deleting', true) }, /** @@ -69,7 +73,7 @@ export const mutations = { * @param state Vuex state * @param uri URI of the entity that failed to be deleted */ - deletingFailed (state, uri) { + deletingFailed (state: State, uri: string) : void { if (state[uri]) Vue.set(state[uri]._meta, 'deleting', false) } } From 522d8ae4164541be93cb88647e31f9011b48906e Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Sun, 6 Dec 2020 11:23:49 +0100 Subject: [PATCH 013/120] convert index to Typescript (partial; WIP) --- src/{index.js => index.ts} | 104 ++++++++++++++++++++++------------- src/interfaces/ApiActions.ts | 9 +-- src/interfaces/StoreData.ts | 1 + src/normalizeEntityUri.ts | 4 +- src/storeModule.ts | 2 +- 5 files changed, 74 insertions(+), 46 deletions(-) rename src/{index.js => index.ts} (81%) diff --git a/src/index.js b/src/index.ts similarity index 81% rename from src/index.js rename to src/index.ts index 2b02d19a..c60f3462 100644 --- a/src/index.js +++ b/src/index.ts @@ -2,11 +2,18 @@ import normalize from 'hal-json-normalizer' import urltemplate from 'url-template' import normalizeEntityUri from './normalizeEntityUri' import StoreValueCreator from './StoreValueCreator' -import StoreValue from './StoreValue.ts' +import StoreValue from './StoreValue' import LoadingStoreValue from './LoadingStoreValue' -import storeModule from './storeModule' -import ServerException from './ServerException.ts' +import storeModule, { State } from './storeModule' +import ServerException from './ServerException' import { createResolvedPromise, wrapPromise } from './QueryablePromise' +import { ExternalConfig } from './interfaces/Config' +import { Store } from 'vuex/types' +import { AxiosInstance } from 'axios' +import Resource, { EmbeddedCollectionType } from './interfaces/Resource' +import StoreData from './interfaces/StoreData' +import ApiActions from './interfaces/ApiActions' +import EmbeddedCollection from './EmbeddedCollection' /** * Defines the API store methods available in all Vue components. The methods can be called as follows: @@ -18,12 +25,12 @@ import { createResolvedPromise, wrapPromise } from './QueryablePromise' * // In the