diff --git a/registry/client/package-lock.json b/registry/client/package-lock.json index 74c4ef00..4f5a318d 100644 --- a/registry/client/package-lock.json +++ b/registry/client/package-lock.json @@ -33,7 +33,6 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz", "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==", - "dev": true, "requires": { "@babel/highlight": "^7.8.3" } @@ -243,7 +242,6 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.8.3.tgz", "integrity": "sha512-R0Bx3jippsbAEtzkpZ/6FIiuzOURPcMjHp+Z6xPe6DtApDJx+w7UYyOLanZqO8+wKR9G10s/FmHXvxaMd9s6Kg==", - "dev": true, "requires": { "@babel/types": "^7.8.3" } @@ -334,8 +332,7 @@ "@babel/helper-validator-identifier": { "version": "7.9.0", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.0.tgz", - "integrity": "sha512-6G8bQKjOh+of4PV/ThDm/rRqlU7+IGoJuofpagU5GlEl29Vv0RGqqt86ZGRV8ZuSOY3o+8yXl5y782SMcG7SHw==", - "dev": true + "integrity": "sha512-6G8bQKjOh+of4PV/ThDm/rRqlU7+IGoJuofpagU5GlEl29Vv0RGqqt86ZGRV8ZuSOY3o+8yXl5y782SMcG7SHw==" }, "@babel/helper-wrap-function": { "version": "7.8.3", @@ -364,7 +361,6 @@ "version": "7.9.0", "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.9.0.tgz", "integrity": "sha512-lJZPilxX7Op3Nv/2cvFdnlepPXDxi29wxteT57Q965oc5R9v86ztx0jfxVrTcBk8C2kcPkkDa2Z4T3ZsPPVWsQ==", - "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.9.0", "chalk": "^2.0.0", @@ -1117,18 +1113,70 @@ "version": "7.9.0", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.9.0.tgz", "integrity": "sha512-BS9JKfXkzzJl8RluW4JGknzpiUV7ZrvTayM6yfqLTVBEnFtyowVIOu6rqxRd5cVO6yGoWf4T8u8dgK9oB+GCng==", - "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.9.0", "lodash": "^4.17.13", "to-fast-properties": "^2.0.0" } }, + "@emotion/cache": { + "version": "10.0.29", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-10.0.29.tgz", + "integrity": "sha512-fU2VtSVlHiF27empSbxi1O2JFdNWZO+2NFHfwO0pxgTep6Xa3uGb+3pVKfLww2l/IBGLNEZl5Xf/++A4wAYDYQ==", + "requires": { + "@emotion/sheet": "0.9.4", + "@emotion/stylis": "0.8.5", + "@emotion/utils": "0.11.3", + "@emotion/weak-memoize": "0.2.5" + } + }, "@emotion/hash": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz", "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==" }, + "@emotion/memoize": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz", + "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==" + }, + "@emotion/serialize": { + "version": "0.11.16", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-0.11.16.tgz", + "integrity": "sha512-G3J4o8by0VRrO+PFeSc3js2myYNOXVJ3Ya+RGVxnshRYgsvErfAOglKAiy1Eo1vhzxqtUvjCyS5gtewzkmvSSg==", + "requires": { + "@emotion/hash": "0.8.0", + "@emotion/memoize": "0.7.4", + "@emotion/unitless": "0.7.5", + "@emotion/utils": "0.11.3", + "csstype": "^2.5.7" + } + }, + "@emotion/sheet": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-0.9.4.tgz", + "integrity": "sha512-zM9PFmgVSqBw4zL101Q0HrBVTGmpAxFZH/pYx/cjJT5advXguvcgjHFTCaIO3enL/xr89vK2bh0Mfyj9aa0ANA==" + }, + "@emotion/stylis": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.8.5.tgz", + "integrity": "sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ==" + }, + "@emotion/unitless": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", + "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==" + }, + "@emotion/utils": { + "version": "0.11.3", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-0.11.3.tgz", + "integrity": "sha512-0o4l6pZC+hI88+bzuaX/6BgOvQVhbt2PfmxauVaYOGgbsAw14wdKyvMCZXnsnsHys94iadcF+RG/wZyx6+ZZBw==" + }, + "@emotion/weak-memoize": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz", + "integrity": "sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA==" + }, "@hapi/address": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/@hapi/address/-/address-2.1.4.tgz", @@ -1424,6 +1472,11 @@ "integrity": "sha512-rouEWBImiRaSJsVA+ITTFM6ZxibuAlTuNOCyxVbwreu6k6+ujs7DfnU9o+PShFhET78pMBl3eH+AGSI5eOTkPA==", "dev": true }, + "@types/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==" + }, "@types/prop-types": { "version": "15.7.3", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.3.tgz", @@ -1979,6 +2032,38 @@ "object.assign": "^4.1.0" } }, + "babel-plugin-emotion": { + "version": "10.0.33", + "resolved": "https://registry.npmjs.org/babel-plugin-emotion/-/babel-plugin-emotion-10.0.33.tgz", + "integrity": "sha512-bxZbTTGz0AJQDHm8k6Rf3RQJ8tX2scsfsRyKVgAbiUPUNIRtlK+7JxP+TAd1kRLABFxe0CFm2VdK4ePkoA9FxQ==", + "requires": { + "@babel/helper-module-imports": "^7.0.0", + "@emotion/hash": "0.8.0", + "@emotion/memoize": "0.7.4", + "@emotion/serialize": "^0.11.16", + "babel-plugin-macros": "^2.0.0", + "babel-plugin-syntax-jsx": "^6.18.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^1.0.5", + "find-root": "^1.1.0", + "source-map": "^0.5.7" + } + }, + "babel-plugin-macros": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-2.8.0.tgz", + "integrity": "sha512-SEP5kJpfGYqYKpBrj5XU3ahw5p5GOHJ0U5ssOSQ/WBVdwkD2Dzlce95exQTs3jOVWPPKLBN2rlEWkCK7dSmLvg==", + "requires": { + "@babel/runtime": "^7.7.2", + "cosmiconfig": "^6.0.0", + "resolve": "^1.12.0" + } + }, + "babel-plugin-syntax-jsx": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz", + "integrity": "sha1-CvMqmm4Tyno/1QaeYtew9Y0NiUY=" + }, "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", @@ -2389,6 +2474,11 @@ "unset-value": "^1.0.0" } }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==" + }, "camel-case": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-3.0.0.tgz", @@ -2415,7 +2505,6 @@ "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", @@ -2702,7 +2791,6 @@ "version": "1.7.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", - "dev": true, "requires": { "safe-buffer": "~5.1.1" } @@ -3047,6 +3135,18 @@ "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", "dev": true }, + "cosmiconfig": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz", + "integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==", + "requires": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.1.0", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.7.2" + } + }, "create-ecdh": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.3.tgz", @@ -3057,6 +3157,17 @@ "elliptic": "^6.0.0" } }, + "create-emotion": { + "version": "10.0.27", + "resolved": "https://registry.npmjs.org/create-emotion/-/create-emotion-10.0.27.tgz", + "integrity": "sha512-fIK73w82HPPn/RsAij7+Zt8eCE8SptcJ3WoRMfxMtjteYxud8GDTKKld7MYwAX2TVhrw29uR1N/bVGxeStHILg==", + "requires": { + "@emotion/cache": "^10.0.27", + "@emotion/serialize": "^0.11.15", + "@emotion/sheet": "0.9.4", + "@emotion/utils": "0.11.3" + } + }, "create-hash": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", @@ -3360,6 +3471,11 @@ "resolved": "https://registry.npmjs.org/diacritic/-/diacritic-0.0.2.tgz", "integrity": "sha1-/CqIe1pbwKCoVPthTHwvIJBh7gQ=" }, + "diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==" + }, "diffie-hellman": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", @@ -3556,6 +3672,15 @@ "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", "dev": true }, + "emotion": { + "version": "10.0.27", + "resolved": "https://registry.npmjs.org/emotion/-/emotion-10.0.27.tgz", + "integrity": "sha512-2xdDzdWWzue8R8lu4G76uWX5WhyQuzATon9LmNeCy/2BHVC6dsEpfhN1a0qhELgtDVdjyEA6J8Y/VlI5ZnaH0g==", + "requires": { + "babel-plugin-emotion": "^10.0.27", + "create-emotion": "^10.0.27" + } + }, "encodeurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", @@ -3609,6 +3734,14 @@ "prr": "~1.0.1" } }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "requires": { + "is-arrayish": "^0.2.1" + } + }, "es-abstract": { "version": "1.17.5", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.5.tgz", @@ -3657,8 +3790,7 @@ "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, "eslint-scope": { "version": "4.0.3", @@ -4182,6 +4314,11 @@ "pkg-dir": "^3.0.0" } }, + "find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==" + }, "find-up": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", @@ -5052,8 +5189,7 @@ "has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" }, "has-symbols": { "version": "1.0.1", @@ -5421,6 +5557,22 @@ "integrity": "sha512-QPMrUVdhTXUII2xcq1pGqXJvIz7qb77TlY9eejQdZJaE2bKMBMBtjkVbDTgovieV591tfZ9fSr6ejCWxzX/gzw==", "dev": true }, + "import-fresh": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.2.tgz", + "integrity": "sha512-cTPNrlvJT6twpYy+YmKUKrTSjWFs3bjYjAhCwm+z4EOCubZxAuO+hHpRN64TqjEaYSHs7tJAE0w1CKMGmsG/lw==", + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "dependencies": { + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==" + } + } + }, "import-local": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-2.0.0.tgz", @@ -5563,6 +5715,11 @@ "integrity": "sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA==", "dev": true }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=" + }, "is-binary-path": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", @@ -5825,6 +5982,11 @@ "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", "dev": true }, + "json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" + }, "json-schema": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", @@ -6015,6 +6177,11 @@ "leven": "^3.1.0" } }, + "lines-and-columns": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz", + "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=" + }, "loader-runner": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.4.0.tgz", @@ -6163,6 +6330,11 @@ } } }, + "memoize-one": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.1.1.tgz", + "integrity": "sha512-HKeeBpWvqiVJD57ZUAsJNm71eHTykffzcLZVYWiVfQeI1rJtuEaS7hQiEpWfVVk18donPwJEcFKIkCmPJNOhHA==" + }, "memory-fs": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", @@ -6941,6 +7113,14 @@ "no-case": "^2.2.0" } }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "requires": { + "callsites": "^3.0.0" + } + }, "parse-asn1": { "version": "5.1.5", "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.5.tgz", @@ -6955,6 +7135,17 @@ "safe-buffer": "^5.1.1" } }, + "parse-json": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.1.0.tgz", + "integrity": "sha512-+mi/lmVVNKFNVyLXV31ERiy2CY5E1/F6QtJFEzoChPRwwngMNXRDQ9GJ5WdE2Z2P4AujsOi0/+2qHID68KwfIQ==", + "requires": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + } + }, "parse-passwd": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", @@ -7012,8 +7203,7 @@ "path-parse": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", - "dev": true + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" }, "path-to-regexp": { "version": "1.8.0", @@ -7026,8 +7216,7 @@ "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 + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==" }, "pbkdf2": { "version": "3.0.17", @@ -7583,6 +7772,19 @@ "redux-saga": "^1.0.0" } }, + "react-diff-viewer": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/react-diff-viewer/-/react-diff-viewer-3.1.1.tgz", + "integrity": "sha512-rmvwNdcClp6ZWdS11m1m01UnBA4OwYaLG/li0dB781e/bQEzsGyj+qewVd6W5ztBwseQ72pO7nwaCcq5jnlzcw==", + "requires": { + "classnames": "^2.2.6", + "create-emotion": "^10.0.14", + "diff": "^4.0.1", + "emotion": "^10.0.14", + "memoize-one": "^5.0.4", + "prop-types": "^15.6.2" + } + }, "react-dom": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.13.1.tgz", @@ -7985,7 +8187,6 @@ "version": "1.15.1", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.15.1.tgz", "integrity": "sha512-84oo6ZTtoTUpjgNEr5SJyzQhzL72gaRodsSfyxC/AXRvwu0Yse9H8eF9IpGo7b8YetZhlI6v7ZQ6bKBFV/6S7w==", - "dev": true, "requires": { "path-parse": "^1.0.6" } @@ -8100,8 +8301,7 @@ "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, "safe-regex": { "version": "1.1.0", @@ -8494,8 +8694,7 @@ "source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" }, "source-map-resolve": { "version": "0.5.3", @@ -8911,7 +9110,6 @@ "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" } @@ -9066,8 +9264,7 @@ "to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", - "dev": true + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=" }, "to-object-path": { "version": "0.3.0", @@ -10011,6 +10208,11 @@ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true }, + "yaml": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.0.tgz", + "integrity": "sha512-yr2icI4glYaNG+KWONODapy2/jDdMSDnrONSjblABjD9B4Z5LgiircSt8m8sRZFNi08kG9Sm0uSHtEmP3zaEGg==" + }, "yargs": { "version": "13.2.4", "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.2.4.tgz", diff --git a/registry/client/package.json b/registry/client/package.json index 9b503a2f..2f283022 100755 --- a/registry/client/package.json +++ b/registry/client/package.json @@ -44,6 +44,7 @@ "ra-data-simple-rest": "^3.9.3", "react": "^16.9.0", "react-admin": "^3.9.5", + "react-diff-viewer": "^3.1.1", "react-dom": "^16.9.0" } } diff --git a/registry/client/src/authEntities/dataTransform.js b/registry/client/src/authEntities/dataTransform.js new file mode 100644 index 00000000..11b30075 --- /dev/null +++ b/registry/client/src/authEntities/dataTransform.js @@ -0,0 +1,13 @@ +import {setOperations} from "../dataProvider"; + +export function transformGet(entity) { + +} + +export function transformSet(entity, operation) { + if (operation === setOperations.update) { + delete entity.id; + delete entity.identifier; + delete entity.provider; + } +} diff --git a/registry/client/src/dataProvider.js b/registry/client/src/dataProvider.js index 8ca31080..4b3a659d 100755 --- a/registry/client/src/dataProvider.js +++ b/registry/client/src/dataProvider.js @@ -6,11 +6,17 @@ import * as apps from './apps/dataTransform'; import * as templates from './templates/dataTransform'; import * as appRoutes from './appRoutes/dataTransform'; import * as settings from './settings/dataTransform'; +import * as authEntities from './authEntities/dataTransform'; import httpClient from './httpClient'; const dataProvider = simpleRestProvider('/api/v1', httpClient); +export const setOperations = Object.freeze({ + create: 'create', + update: 'update', +}); + const myDataProvider = { ...dataProvider, getList: (resource, params) => { @@ -42,7 +48,7 @@ const myDataProvider = { update: (resource, params) => { params.id = encodeURIComponent(params.id); - transformSetter(resource, params.data); + transformSetter(resource, params.data, setOperations.update); delete params.data.name; return dataProvider.update(resource, params).then(v => { @@ -54,7 +60,7 @@ const myDataProvider = { transformSetter(resource, params.data); return dataProvider.create(resource, params).then(v => { - transformGetter(resource, v.data); + transformGetter(resource, v.data, setOperations.create); return v; }); }, @@ -94,26 +100,32 @@ function transformGetter(resource, data) { case 'settings': settings.transformGet(data); break; + case 'auth_entities': + authEntities.transformGet(data); + break; default: } } -function transformSetter(resource, data) { +function transformSetter(resource, data, operation) { switch (resource) { case 'app': - apps.transformSet(data); + apps.transformSet(data, operation); break; case 'shared_props': - sharedProps.transformSet(data); + sharedProps.transformSet(data, operation); break; case 'template': - templates.transformSet(data); + templates.transformSet(data, operation); break; case 'route': - appRoutes.transformSet(data); + appRoutes.transformSet(data, operation); break; case 'settings': - settings.transformSet(data); + settings.transformSet(data, operation); + break; + case 'auth_entities': + authEntities.transformSet(data, operation); break; default: } diff --git a/registry/client/src/index.js b/registry/client/src/index.js index 36483b81..208839bb 100755 --- a/registry/client/src/index.js +++ b/registry/client/src/index.js @@ -13,6 +13,7 @@ import templates from './templates'; import appRoutes from './appRoutes'; import authEntities from './authEntities'; import settings from './settings'; +import versioning from './versioning'; render( , , , + , ]} , document.getElementById('root') diff --git a/registry/client/src/utils/json.js b/registry/client/src/utils/json.js new file mode 100644 index 00000000..70319d01 --- /dev/null +++ b/registry/client/src/utils/json.js @@ -0,0 +1,31 @@ + +import _ from 'lodash/fp'; + +/** + * Original source code was taken from {@link https://github.com/prototypejs/prototype/blob/5fddd3e/src/prototype/lang/string.js#L702} + */ +const isJSON = (str) => { + if (/^\s*$/.test(str)) return false; + + str = str.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@'); + str = str.replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']'); + str = str.replace(/(?:^|:|,)(?:\s*\[)+/g, ''); + + return (/^[\],:{}\s]*$/).test(str); +}; + +const parse = (value) => { + if (_.isString(value) && isJSON(value)) { + return JSON.parse(value); + } + + return value; +} + +export function parseJSON(value) { + return _.cond([ + [_.isArray, _.map(_.mapValues(parseJSON))], + [_.isObject, _.mapValues(parseJSON)], + [_.stubTrue, parse] + ])(value); +}; diff --git a/registry/client/src/versioning/List.js b/registry/client/src/versioning/List.js new file mode 100644 index 00000000..7561b8b7 --- /dev/null +++ b/registry/client/src/versioning/List.js @@ -0,0 +1,134 @@ +import React, {Children, cloneElement, useState, useCallback} from 'react'; +import {makeStyles} from '@material-ui/core'; +import ReactDiffViewer from 'react-diff-viewer'; +import { + List, + Datagrid, + Button, + TextField, + SelectInput, + TextInput, + Filter, + FunctionField, + useRefresh, + useNotify +} from 'react-admin'; +import {parseJSON} from '../utils/json'; +import {SettingsBackupRestore} from "@material-ui/icons"; + +const ListActionsToolbar = ({children, ...props}) => { + const classes = makeStyles({ + toolbar: { + alignItems: 'center', + display: 'flex', + marginTop: -1, + marginBottom: -1, + }, + }); + + return ( +
+ {Children.map(children, button => cloneElement(button, props))} +
+ ); +}; + +const MyFilter = (props) => ( + + + + + +); + +const beautifyJson = v => JSON.stringify(parseJSON(JSON.parse(v)), null, 2); + + +const MyPanel = ({ id, record, resource }) => ( + +); + +const RevertButton = ({ + record, + ...rest +}) => { + const [disabled, setDisabled] = useState(false); + const refresh = useRefresh(); + const notify = useNotify(); + + const handleClick = useCallback(() => { + if (confirm(`Are you sure that you want to revert change with ID "${record.id}"?`)) { + setDisabled(true); + fetch(`/api/v1/versioning/${record.id}/revert`, {method: 'POST'}).then(async res => { + setDisabled(false); + if (!res.ok) { + if (res.status < 500) { + const resInfo = await res.json(); + return notify(resInfo.reason, 'error', { smart_count: 1 }); + } + throw new Error(`Unexpected network error. Returned code "${res.status}"`); + } + notify('Change was successfully reverted', 'info', { smart_count: 1 }); + refresh(); + }).catch(err => { + setDisabled(false); + notify('Oops! Something went wrong.', 'error', { smart_count: 1 }); + console.error(err); + }); + } + }, []); + + return ( + + ); +}; + +const PostList = props => { + return ( + } + exporter={false} + perPage={25} + bulkActionButtons={false} + > + } > + + + + record.data && record.data_after ? 'UPDATE' : record.data ? 'DELETE' : 'CREATE'} /> + + new Date(record.created_at * 1000).toLocaleString()} /> + + + + + + ); +}; + +export default PostList; diff --git a/registry/client/src/versioning/dataTransform.js b/registry/client/src/versioning/dataTransform.js new file mode 100644 index 00000000..d5369c7c --- /dev/null +++ b/registry/client/src/versioning/dataTransform.js @@ -0,0 +1,7 @@ +export function transformGet(setting) { + +} + +export function transformSet(setting) { + +} diff --git a/registry/client/src/versioning/index.js b/registry/client/src/versioning/index.js new file mode 100644 index 00000000..d3a24a81 --- /dev/null +++ b/registry/client/src/versioning/index.js @@ -0,0 +1,10 @@ +import Icon from '@material-ui/icons/History'; +import React, {Children, cloneElement} from 'react'; + +import List from './List'; + +export default { + list: List, + icon: Icon, + options: {label: 'History'}, +}; diff --git a/registry/package.json b/registry/package.json index f1ce4426..27b3ef05 100644 --- a/registry/package.json +++ b/registry/package.json @@ -51,9 +51,10 @@ "supertest": "4.0.2", "timekeeper": "^2.2.0", "ts-node": "^8.10.2", - "typescript": "3.7.5" + "typescript": "^3.9.7" }, "dependencies": { + "@namecheap/error-extender": "^1.2.0", "axios": "^0.19.0", "bcrypt": "^5.0.0", "body-parser": "^1.19.0", diff --git a/registry/server/app.ts b/registry/server/app.ts index 02ede77c..7c12b364 100644 --- a/registry/server/app.ts +++ b/registry/server/app.ts @@ -38,6 +38,7 @@ export default (withAuth: boolean = true) => { app.use('/api/v1/route', authMw, routes.appRoutes); app.use('/api/v1/shared_props', authMw, routes.sharedProps); app.use('/api/v1/auth_entities', authMw, routes.authEntities); + app.use('/api/v1/versioning', authMw, routes.versioning); app.use('/api/v1/settings', routes.settings(authMw)); app.use(errorHandler); diff --git a/registry/server/appRoutes/routes/createAppRoute.ts b/registry/server/appRoutes/routes/createAppRoute.ts index eab794e7..1f73ded6 100644 --- a/registry/server/appRoutes/routes/createAppRoute.ts +++ b/registry/server/appRoutes/routes/createAppRoute.ts @@ -27,25 +27,27 @@ const createAppRoute = async (req: Request, res: Response) => { ...appRoute } = req.body; - const savedAppRouteId = await db.transaction(async (transaction) => { - const [appRouteId] = await db('routes').insert(appRoute).transacting(transaction); + let savedAppRouteId: number; + + await db.versioning(req.user, {type: 'routes'}, async (transaction) => { + [savedAppRouteId] = await db('routes').insert(appRoute).transacting(transaction); await db.batchInsert('route_slots', _.compose( _.map((appRouteSlotName) => _.compose( stringifyJSON(['props']), - _.assign({ name: appRouteSlotName, routeId: appRouteId }), + _.assign({ name: appRouteSlotName, routeId: savedAppRouteId }), _.get(appRouteSlotName) )(appRouteSlots)), _.keys, )(appRouteSlots)).transacting(transaction); - return appRouteId; + return savedAppRouteId; }); const savedAppRoute = await db .select('routes.id as routeId', 'route_slots.id as routeSlotId', 'routes.*', 'route_slots.*') .from('routes') - .where('routeId', savedAppRouteId) + .where('routeId', savedAppRouteId!) .join('route_slots', { 'route_slots.routeId': 'routes.id' }); diff --git a/registry/server/appRoutes/routes/deleteAppRoute.ts b/registry/server/appRoutes/routes/deleteAppRoute.ts index 35714f79..6281bee4 100644 --- a/registry/server/appRoutes/routes/deleteAppRoute.ts +++ b/registry/server/appRoutes/routes/deleteAppRoute.ts @@ -4,6 +4,7 @@ import { } from 'express'; import Joi from 'joi'; import _ from 'lodash/fp'; +import * as httpErrors from '../../errorHandler/httpErrors'; import db from '../../db'; import validateRequestFactory from '../../common/services/validateRequest'; @@ -25,16 +26,15 @@ const validateRequestBeforeDeleteAppRoute = validateRequestFactory([{ const deleteAppRoute = async (req: Request, res: Response) => { const appRouteId = req.params.id; - const count = await db.transaction(async (transaction) => { + await db.versioning(req.user, {type: 'routes', id: appRouteId}, async (transaction) => { await db('route_slots').where('routeId', appRouteId).delete().transacting(transaction); - return await db('routes').where('id', appRouteId).delete().transacting(transaction); + const count = await db('routes').where('id', appRouteId).delete().transacting(transaction); + if (!count) { + throw new httpErrors.NotFoundError() + } }); - if (count) { - res.status(204).send(); - } else { - res.status(404).send('Not found'); - } + res.status(204).send(); }; export default [validateRequestBeforeDeleteAppRoute, deleteAppRoute]; diff --git a/registry/server/appRoutes/routes/updateAppRoute.ts b/registry/server/appRoutes/routes/updateAppRoute.ts index 985e316b..3cfb6f51 100644 --- a/registry/server/appRoutes/routes/updateAppRoute.ts +++ b/registry/server/appRoutes/routes/updateAppRoute.ts @@ -50,7 +50,7 @@ const updateAppRoute = async (req: Request, res: Re return; } - await db.transaction(async (transaction) => { + await db.versioning(req.user, {type: 'routes', id: appRouteId}, async (transaction) => { await db('routes').where('id', appRouteId).update(appRoute).transacting(transaction); if (!_.isEmpty(appRouteSlots)) { diff --git a/registry/server/apps/routes/createApp.ts b/registry/server/apps/routes/createApp.ts index 2d3cb707..20c9669d 100644 --- a/registry/server/apps/routes/createApp.ts +++ b/registry/server/apps/routes/createApp.ts @@ -23,7 +23,11 @@ const validateRequestBeforeCreateApp = validateRequestFactory([{ const createApp = async (req: Request, res: Response): Promise => { const app = req.body; - await db('apps').insert(stringifyJSON(['dependencies', 'props', 'ssr', 'configSelector'], app)); + await db.versioning(req.user, {type: 'apps', id: app.name}, async (trx) => { + await db('apps') + .insert(stringifyJSON(['dependencies', 'props', 'ssr', 'configSelector'], app)) + .transacting(trx); + }); const [savedApp] = await db.select().from('apps').where('name', app.name); diff --git a/registry/server/apps/routes/deleteApp.ts b/registry/server/apps/routes/deleteApp.ts index b732dc1d..8c3703fb 100644 --- a/registry/server/apps/routes/deleteApp.ts +++ b/registry/server/apps/routes/deleteApp.ts @@ -4,6 +4,7 @@ import { } from 'express'; import Joi from 'joi'; import _ from 'lodash/fp'; +import * as httpErrors from '../../errorHandler/httpErrors'; import db from '../../db'; import validateRequestFactory from '../../common/services/validateRequest'; @@ -25,13 +26,14 @@ const validateRequestBeforeDeleteApp = validateRequestFactory([{ const deleteApp = async (req: Request, res: Response): Promise => { const appName = req.params.name; - const count = await db('apps').where('name', appName).delete(); + await db.versioning(req.user, {type: 'apps', id: appName}, async (trx) => { + const count = await db('apps').where('name', appName).delete().transacting(trx); + if (!count) { + throw new httpErrors.NotFoundError() + } + }); - if (count) { - res.status(204).send(); - } else { - res.status(404).send('Not found'); - } + res.status(204).send(); }; export default [validateRequestBeforeDeleteApp, deleteApp]; diff --git a/registry/server/apps/routes/updateApp.ts b/registry/server/apps/routes/updateApp.ts index 59c988bb..a17395d7 100644 --- a/registry/server/apps/routes/updateApp.ts +++ b/registry/server/apps/routes/updateApp.ts @@ -44,7 +44,12 @@ const updateApp = async (req: Request, res: Response): P return; } - await db('apps').where({ name: appName }).update(stringifyJSON(['dependencies', 'props', 'ssr', 'configSelector'], app)); + await db.versioning(req.user, {type: 'apps', id: appName}, async (trx) => { + await db('apps') + .where({ name: appName }) + .update(stringifyJSON(['dependencies', 'props', 'ssr', 'configSelector'], app)) + .transacting(trx); + }); const [updatedApp] = await db.select().from('apps').where('name', appName); diff --git a/registry/server/auth.ts b/registry/server/auth.ts index 8326c143..b414e664 100644 --- a/registry/server/auth.ts +++ b/registry/server/auth.ts @@ -12,6 +12,12 @@ import {SettingsService} from "./settings/services/SettingsService"; import {SettingKeys} from "./settings/interfaces"; import urljoin from 'url-join'; +export interface User { + authEntityId: number; + identifier: string; + role: string; +} + export default (app: Express, settingsService: SettingsService, config: any): RequestHandler => { const SessionKnex = sessionKnex(session); const sessionConfig = Object.assign({ @@ -241,7 +247,7 @@ export default (app: Express, settingsService: SettingsService, config: any): Re }; } -async function getEntityWithCreds(provider: string, identifier: string, secret: string|null):Promise { +async function getEntityWithCreds(provider: string, identifier: string, secret: string|null):Promise { const user = await db.select().from('auth_entities') .first('identifier', 'id', 'role', 'secret') .where({ diff --git a/registry/server/authEntities/routes/create.ts b/registry/server/authEntities/routes/create.ts index e7811f6a..3693964c 100644 --- a/registry/server/authEntities/routes/create.ts +++ b/registry/server/authEntities/routes/create.ts @@ -23,8 +23,13 @@ const createSharedProps = async (req: Request, res: Response): Promise => input.secret = await bcrypt.hash(input.secret, await bcrypt.genSalt()); } - const [recordId] = await db('auth_entities').insert(req.body); - const [savedRecord] = await db.select().from('auth_entities').where('id', recordId); + let recordId: number; + await db.versioning(req.user, {type: 'auth_entities'}, async (trx) => { + [recordId] = await db('auth_entities').insert(req.body).transacting(trx); + return recordId; + }); + + const [savedRecord] = await db.select().from('auth_entities').where('id', recordId!); delete savedRecord.secret; res.status(200).send(preProcessResponse(savedRecord)); diff --git a/registry/server/authEntities/routes/deleteRoute.ts b/registry/server/authEntities/routes/deleteRoute.ts index 6cd93e0a..77fb2c08 100644 --- a/registry/server/authEntities/routes/deleteRoute.ts +++ b/registry/server/authEntities/routes/deleteRoute.ts @@ -7,6 +7,7 @@ import _ from 'lodash/fp'; import db from '../../db'; import validateRequestFactory from '../../common/services/validateRequest'; +import * as httpErrors from '../../errorHandler/httpErrors'; type RequestParams = { id: string @@ -20,13 +21,14 @@ const validateRequest = validateRequestFactory([{ }]); const deleteRecord = async (req: Request, res: Response): Promise => { - const count = await db('auth_entities').where('id', req.params.id).delete(); + await db.versioning(req.user, {type: 'auth_entities', id: req.params.id}, async (trx) => { + const count = await db('auth_entities').where('id', req.params.id).delete().transacting(trx); + if (!count) { + throw new httpErrors.NotFoundError() + } + }); - if (count) { - res.status(204).send(); - } else { - res.status(404).send('Not found'); - } + res.status(204).send(); }; export default [validateRequest, deleteRecord]; diff --git a/registry/server/authEntities/routes/update.ts b/registry/server/authEntities/routes/update.ts index e49ec583..2b759098 100644 --- a/registry/server/authEntities/routes/update.ts +++ b/registry/server/authEntities/routes/update.ts @@ -44,7 +44,10 @@ const updateSharedProps = async (req: Request, res: Response): Pr input.secret = await bcrypt.hash(input.secret, await bcrypt.genSalt()); } - await db('auth_entities').where({ id: recordId }).update(input); + await db.versioning(req.user, {type: 'auth_entities', id: recordId}, async (trx) => { + await db('auth_entities').where({ id: recordId }).update(input).transacting(trx); + }); + const [updatedRecord] = await db.select().from('auth_entities').where('id', recordId); delete updatedRecord.secret; diff --git a/registry/server/db.ts b/registry/server/db.ts deleted file mode 100644 index 0e9df809..00000000 --- a/registry/server/db.ts +++ /dev/null @@ -1,61 +0,0 @@ -'use strict'; - -import knex from 'knex'; -import config from 'config'; - -const client: string = config.get('database.client'); - -const knexConf: knex.Config = { // after: const knex = require('knex')({client: 'mysql'}); - client: client, - connection: config.get('database.connection'), - /** - * Sqlite does not support inserting default values - * That is why we added it - */ - useNullAsDefault: true, -}; - -if (client === 'mysql') { - knexConf.pool = { - afterCreate: (conn: any, done: Function) => { - conn.query('SET time_zone="+00:00";', (err: Error) => done(err, conn)) - } - }; -} else if (client === 'sqlite3'){ - knexConf.pool = { - afterCreate: (conn: any, done: Function) => { - conn.run('PRAGMA foreign_keys = ON;', (err: Error) => done(err, conn)) - } - }; -} - -knex.QueryBuilder.extend('range', function (this: any, range: string|null|undefined): knex.QueryBuilder { - if (typeof range !== 'string') { - return this.client.transaction(async (trx: any) => { - const res = await this.transacting(trx); - return {data: res, pagination: {total: res.length}} - }); - } - - const input = JSON.parse(range); - - const countQuery = new this.constructor(this.client) - .count('* as total') - .from( - this.clone() - .offset(0) - .as('__count__query__'), - ) - .first(); - - // This will paginate the data itself - this.offset(parseInt(input[0])).limit(parseInt(input[1]) - parseInt(input[0]) + 1); - - return this.client.transaction(async (trx: any) => { - const res = await this.transacting(trx); - const { total } = await countQuery.transacting(trx); - return {data: res, pagination: { total }} - }); -}); - -export default knex(knexConf); diff --git a/registry/server/db/index.ts b/registry/server/db/index.ts new file mode 100644 index 00000000..311b9437 --- /dev/null +++ b/registry/server/db/index.ts @@ -0,0 +1,43 @@ +'use strict'; + +import knex from 'knex'; +import config from 'config'; +import rangeExtender from './range'; +import addVersioning from './versioning'; + +const client: string = config.get('database.client'); + +const knexConf: knex.Config = { // after: const knex = require('knex')({client: 'mysql'}); + client: client, + connection: config.get('database.connection'), + /** + * Sqlite does not support inserting default values + * That is why we added it + */ + useNullAsDefault: true, +}; + +if (client === 'mysql') { + knexConf.pool = { + afterCreate: (conn: any, done: Function) => { + conn.query('SET time_zone="+00:00";', (err: Error) => done(err, conn)) + } + }; +} else if (client === 'sqlite3'){ + knexConf.pool = { + afterCreate: (conn: any, done: Function) => { + conn.run('PRAGMA foreign_keys = ON;', (err: Error) => done(err, conn)) + } + }; +} + +rangeExtender(knex); + +export {VersionedKnex} from './versioning'; + +export function dbFactory(conf: knex.Config) { + const knexInstance = knex(conf); + return addVersioning(knexInstance); +} + +export default dbFactory(knexConf); diff --git a/registry/server/db/range.ts b/registry/server/db/range.ts new file mode 100644 index 00000000..ff8f2ef8 --- /dev/null +++ b/registry/server/db/range.ts @@ -0,0 +1,32 @@ +import type Knex from 'knex'; + +export default function (knex: typeof Knex) { + knex.QueryBuilder.extend('range', function (this: any, range: string|null|undefined) { + if (typeof range !== 'string') { + return this.client.transaction(async (trx: any) => { + const res = await this.transacting(trx); + return {data: res, pagination: {total: res.length}} + }); + } + + const input = JSON.parse(range); + + const countQuery = new this.constructor(this.client) + .count('* as total') + .from( + this.clone() + .offset(0) + .as('__count__query__'), + ) + .first(); + + // This will paginate the data itself + this.offset(parseInt(input[0])).limit(parseInt(input[1]) - parseInt(input[0]) + 1); + + return this.client.transaction(async (trx: any) => { + const res = await this.transacting(trx); + const { total } = await countQuery.transacting(trx); + return {data: res, pagination: { total }} + }); + }); +} diff --git a/registry/server/db/versioning.ts b/registry/server/db/versioning.ts new file mode 100644 index 00000000..96f12d7b --- /dev/null +++ b/registry/server/db/versioning.ts @@ -0,0 +1,19 @@ +import type Knex from 'knex'; +import type {Transaction} from 'knex'; + +import versioningService, {OperationConf} from '../versioning/services/Versioning'; + +export interface VersionedKnex extends Knex { + versioning(user: any, conf: OperationConf, callback: (transaction: Transaction) => Promise): Promise; +} + +export default function (knex: Knex|any): VersionedKnex { + if (knex.versioning) { + return knex; + } + + versioningService.setDb(knex); + knex.versioning = versioningService.logOperation; + + return knex; +} diff --git a/registry/server/errorHandler/httpErrors.ts b/registry/server/errorHandler/httpErrors.ts new file mode 100644 index 00000000..5f19aac1 --- /dev/null +++ b/registry/server/errorHandler/httpErrors.ts @@ -0,0 +1,4 @@ +import extendError from '@namecheap/error-extender'; + +export const HttpError = extendError('HttpError'); +export const NotFoundError = extendError('NotFoundError', {parent: HttpError}); diff --git a/registry/server/errorHandler/index.ts b/registry/server/errorHandler/index.ts index f3024ce9..1cec7a85 100644 --- a/registry/server/errorHandler/index.ts +++ b/registry/server/errorHandler/index.ts @@ -1,9 +1,15 @@ import uuidv4 from 'uuid/v4'; import {Request, Response, NextFunction} from 'express'; +import * as httpErrors from './httpErrors'; import noticeError from './noticeError'; async function errorHandler(error: Error, req: Request, res: Response, next: NextFunction): Promise { + if (error instanceof httpErrors.HttpError) { + res.status(404).send('Not found'); + return; + } + const errorId = uuidv4(); noticeError(error, { diff --git a/registry/server/migrations/20201105155522_versioning.ts b/registry/server/migrations/20201105155522_versioning.ts new file mode 100644 index 00000000..bbcb783f --- /dev/null +++ b/registry/server/migrations/20201105155522_versioning.ts @@ -0,0 +1,20 @@ +import * as Knex from "knex"; + + +export async function up(knex: Knex): Promise { + return knex.schema.createTable('versioning', table => { + table.increments('id'); + table.string('entity_type').notNullable(); + table.string('entity_id').notNullable(); + table.text('data'); + table.text('data_after'); + table.string('created_by').notNullable(); + table.timestamp('created_at').defaultTo(knex.fn.now()); + }); +} + + +export async function down(knex: Knex): Promise { + return knex.schema.dropTable('versioning'); +} + diff --git a/registry/server/routes/index.ts b/registry/server/routes/index.ts index b0ab1938..59f381ce 100644 --- a/registry/server/routes/index.ts +++ b/registry/server/routes/index.ts @@ -5,3 +5,4 @@ export { default as appRoutes } from '../appRoutes/routes'; export { default as sharedProps } from '../sharedProps/routes'; export { default as authEntities } from '../authEntities/routes'; export { default as settings } from '../settings/routes'; +export { default as versioning } from '../versioning/routes'; diff --git a/registry/server/seeds/00_cleanup.ts b/registry/server/seeds/00_cleanup.ts index 84eeee01..b17668de 100644 --- a/registry/server/seeds/00_cleanup.ts +++ b/registry/server/seeds/00_cleanup.ts @@ -9,6 +9,7 @@ export async function seed(knex: Knex): Promise { await knex('routes').transacting(trx).truncate(); await knex('apps').transacting(trx).truncate(); await knex('templates').transacting(trx).truncate(); + await knex('shared_props').transacting(trx).truncate(); } finally { isMySQL(knex) && await knex.schema.raw('SET FOREIGN_KEY_CHECKS = 1;').transacting(trx); } diff --git a/registry/server/settings/routes/updateSetting.ts b/registry/server/settings/routes/updateSetting.ts index a4806f0e..23cf3fc0 100644 --- a/registry/server/settings/routes/updateSetting.ts +++ b/registry/server/settings/routes/updateSetting.ts @@ -32,8 +32,16 @@ const validateRequest = validateRequestFactory([ ]); const updateSetting = async (req: Request, res: Response): Promise => { - await db('settings').where('key', req.params.key).update('value', JSON.stringify(req.body.value)); - const [updated] = await db.select().from('settings').where('key', req.params.key); + const settingKey = req.params.key; + + await db.versioning(req.user, {type: 'settings', id: settingKey}, async (trx) => { + await db('settings') + .where('key', settingKey) + .update('value', JSON.stringify(req.body.value)) + .transacting(trx); + }); + + const [updated] = await db.select().from('settings').where('key', settingKey); res.status(200).send(preProcessResponse(updated)); }; diff --git a/registry/server/sharedProps/routes/createSharedProps.ts b/registry/server/sharedProps/routes/createSharedProps.ts index 6b4510a8..980189a0 100644 --- a/registry/server/sharedProps/routes/createSharedProps.ts +++ b/registry/server/sharedProps/routes/createSharedProps.ts @@ -18,7 +18,10 @@ const validateRequest = validateRequestFactory([{ const createSharedProps = async (req: Request, res: Response): Promise => { const sharedProps = req.body; - await db('shared_props').insert(stringifyJSON(['props'], sharedProps)); + await db.versioning(req.user, {type: 'shared_props', id: sharedProps.name}, async (trx) => { + await db('shared_props').insert(stringifyJSON(['props'], sharedProps)).transacting(trx); + }); + const [savedSharedProps] = await db.select().from('shared_props').where('name', sharedProps.name); res.status(200).send(preProcessResponse(savedSharedProps)); diff --git a/registry/server/sharedProps/routes/deleteSharedProps.ts b/registry/server/sharedProps/routes/deleteSharedProps.ts index ac453db0..c7f7d3ff 100644 --- a/registry/server/sharedProps/routes/deleteSharedProps.ts +++ b/registry/server/sharedProps/routes/deleteSharedProps.ts @@ -8,6 +8,7 @@ import _ from 'lodash/fp'; import db from '../../db'; import validateRequestFactory from '../../common/services/validateRequest'; import { sharedPropsNameSchema } from '../interfaces'; +import * as httpErrors from "../../errorHandler/httpErrors"; type RequestParams = { name: string @@ -21,13 +22,14 @@ const validateRequest = validateRequestFactory([{ }]); const deleteSharedProps = async (req: Request, res: Response): Promise => { - const count = await db('shared_props').where('name', req.params.name).delete(); + await db.versioning(req.user, {type: 'shared_props', id: req.params.name}, async (trx) => { + const count = await db('shared_props').where('name', req.params.name).delete().transacting(trx); + if (!count) { + throw new httpErrors.NotFoundError() + } + }); - if (count) { - res.status(204).send(); - } else { - res.status(404).send('Not found'); - } + res.status(204).send(); }; export default [validateRequest, deleteSharedProps]; diff --git a/registry/server/sharedProps/routes/updateSharedProps.ts b/registry/server/sharedProps/routes/updateSharedProps.ts index da11e8d6..598265cf 100644 --- a/registry/server/sharedProps/routes/updateSharedProps.ts +++ b/registry/server/sharedProps/routes/updateSharedProps.ts @@ -41,7 +41,13 @@ const updateSharedProps = async (req: Request, res: Response): Pr return; } - await db('shared_props').where({ name: sharedPropsName }).update(stringifyJSON(['props'], sharedProps)); + await db.versioning(req.user, {type: 'shared_props', id: sharedPropsName}, async (trx) => { + await db('shared_props') + .where({ name: sharedPropsName }) + .update(stringifyJSON(['props'], sharedProps)) + .transacting(trx); + }); + const [updatedSharedProps] = await db.select().from('shared_props').where('name', sharedPropsName); res.status(200).send(preProcessResponse(updatedSharedProps)); diff --git a/registry/server/templates/routes/createTemplate.ts b/registry/server/templates/routes/createTemplate.ts index a1167916..4844cbdb 100644 --- a/registry/server/templates/routes/createTemplate.ts +++ b/registry/server/templates/routes/createTemplate.ts @@ -18,7 +18,9 @@ const validateRequestBeforeCreateTemplate = validateRequestFactory([{ const createTemplate = async (req: Request, res: Response): Promise => { const template = req.body; - await db('templates').insert(template); + await db.versioning(req.user, {type: 'templates', id: template.name}, async (trx) => { + await db('templates').insert(template).transacting(trx); + }); const [savedTemplate] = await db.select().from