From 3717962b52a049b93d42e428cb5f9a8613bbf87d Mon Sep 17 00:00:00 2001 From: Jacob Fletcher Date: Mon, 22 Jun 2020 11:35:05 -0400 Subject: [PATCH 001/125] configures @trbl/eslint-config --- .eslintrc.js | 78 +-------------- package.json | 7 +- src/webpack/getWebpackDevConfig.js | 10 +- yarn.lock | 147 ++++++++++++++++++++++++----- 4 files changed, 137 insertions(+), 105 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index e6a01cd77..a6eaf41b8 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,80 +1,4 @@ module.exports = { parser: "babel-eslint", - env: { - browser: true, - es6: true, - jest: true, - }, - extends: 'airbnb', - parserOptions: { - ecmaFeatures: { - jsx: true, - }, - ecmaVersion: 2018, - sourceType: 'module', - }, - plugins: [ - 'react', - 'react-hooks', - ], - rules: { - "import/no-unresolved": [ - 2, - { - ignore: [ - 'payload/config', - 'payload/unsanitizedConfig', - ] - }], - "react/jsx-filename-extension": [ - 1, - { - "extensions": [ - ".js", - ".jsx" - ] - } - ], - "no-console": 0, - "camelcase": 0, - "arrow-body-style": 0, - "jsx-a11y/anchor-is-valid": [ - "error", - { - "aspects": [ - "invalidHref", - "preferButton" - ] - } - ], - "jsx-a11y/click-events-have-key-events": 0, - "jsx-a11y/label-has-for": [ - 2, - { - "components": [ - "Label" - ], - "required": { - "every": [ - "id" - ] - }, - "allowChildren": false - } - ], - "react-hooks/rules-of-hooks": "error", - "react-hooks/exhaustive-deps": "warn", - "react/no-array-index-key": 0, - "max-len": 0, - "react/no-danger": 0, - "import/prefer-default-export": 0, - "no-throw-literal": 0, - "react/jsx-max-props-per-line": [ - 1, - { - "maximum": 1 - } - ], - "linebreak-style": ["off"] - }, + extends: "@trbl", }; diff --git a/package.json b/package.json index a12086029..c5f5715ef 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "test:int": "cross-env PAYLOAD_CONFIG_PATH=demo/payload.config.js NODE_ENV=test jest --forceExit --runInBand", "cov": "npm run core:build && node ./node_modules/jest/bin/jest.js src/tests --coverage", "dev": "PAYLOAD_CONFIG_PATH=demo/payload.config.js nodemon demo/server.js", - "lint": "eslint **/*.js", + "lint": "eslint .", "debug": "nodemon --inspect demo/server.js", "debug:test:int": "node --inspect-brk node_modules/.bin/jest --runInBand" }, @@ -106,14 +106,17 @@ }, "devDependencies": { "@babel/plugin-proposal-optional-chaining": "^7.8.3", + "@trbl/eslint-config": "^1.2.4", "autoprefixer": "^9.7.4", "babel-core": "^7.0.0-bridge.0", "babel-eslint": "^10.0.1", "cross-env": "^7.0.2", "css-loader": "^1.0.0", "eslint": "^6.8.0", - "eslint-config-airbnb": "^17.1.0", + "eslint-loader": "^4.0.2", "eslint-plugin-import": "^2.20.0", + "eslint-plugin-jest": "^23.16.0", + "eslint-plugin-jest-dom": "^3.0.1", "eslint-plugin-jsx-a11y": "^6.2.1", "eslint-plugin-react": "^7.18.0", "eslint-plugin-react-hooks": "^2.3.0", diff --git a/src/webpack/getWebpackDevConfig.js b/src/webpack/getWebpackDevConfig.js index 7dc8f81d4..e7ca78b6f 100644 --- a/src/webpack/getWebpackDevConfig.js +++ b/src/webpack/getWebpackDevConfig.js @@ -34,7 +34,7 @@ module.exports = (config) => { { test: /\.js$/, exclude: /node_modules/, - use: { + use: [{ loader: 'babel-loader', options: { presets: [ @@ -58,6 +58,14 @@ module.exports = (config) => { ], }, }, + // { + // loader: 'eslint-loader', + // options: { + // fix: true, + // emitWarning: true, + // }, + // } + ], }, { // "oneOf" will traverse all following loaders until one will diff --git a/yarn.lock b/yarn.lock index abe820843..ce4149bac 100644 --- a/yarn.lock +++ b/yarn.lock @@ -913,6 +913,13 @@ dependencies: regenerator-runtime "^0.13.4" +"@babel/runtime@^7.9.6": + version "7.10.3" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.10.3.tgz#670d002655a7c366540c67f6fd3342cd09500364" + integrity sha512-RzGO0RLSdokm9Ipe/YD+7ww8X2Ro79qiXZF3HU9ljrM+qnJmH1Vqth+hbiQZy761LnMJTMitHDuKVYTk3k4dLw== + dependencies: + regenerator-runtime "^0.13.4" + "@babel/template@^7.10.1": version "7.10.1" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.10.1.tgz#e167154a94cb5f14b28dc58f5356d2162f539811" @@ -1290,6 +1297,15 @@ prop-types "^15.6.2" tippy.js "^6.2.0" +"@trbl/eslint-config@^1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@trbl/eslint-config/-/eslint-config-1.2.4.tgz#81206ed312d52057f2a58a594d9fc17877260e85" + integrity sha512-t2FX1LdJno9q+JYYTpLTUlGg5X9+1zjsUMvyo8KTdJBgPqIEYdLSEKl7YUkqDwSSLeWrFUPtfVYIuII6Us1dSg== + dependencies: + confusing-browser-globals "^1.0.9" + object.assign "^4.1.0" + object.entries "^1.1.1" + "@trbl/react-collapsibles@^0.1.0": version "0.1.0" resolved "https://registry.yarnpkg.com/@trbl/react-collapsibles/-/react-collapsibles-0.1.0.tgz#81c5590b505c1a155a9be68b8f6c00b242e0b555" @@ -1445,6 +1461,11 @@ resolved "https://registry.yarnpkg.com/@types/js-cookie/-/js-cookie-2.2.6.tgz#f1a1cb35aff47bc5cfb05cb0c441ca91e914c26f" integrity sha512-+oY0FDTO2GYKEV0YPvSshGq9t7YozVkgvXLty7zogQNuCxBhT9/3INX9Q7H1aRZ4SUDRXAKlJuA4EA5nTt7SNw== +"@types/json-schema@^7.0.3": + version "7.0.5" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.5.tgz#dcce4430e64b443ba8945f0290fb564ad5bac6dd" + integrity sha512-7+2BITlgjgDhH0vvwZU/HZJVyk+2XUlvxXe8dFMedNX/aMkaOq++rMAFXc0tM7ij15QaWlbdQASBR9dihi+bDQ== + "@types/lockfile@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@types/lockfile/-/lockfile-1.0.1.tgz#434a3455e89843312f01976e010c60f1bcbd56f7" @@ -1524,6 +1545,29 @@ dependencies: "@types/yargs-parser" "*" +"@typescript-eslint/experimental-utils@^2.5.0": + version "2.34.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-2.34.0.tgz#d3524b644cdb40eebceca67f8cf3e4cc9c8f980f" + integrity sha512-eS6FTkq+wuMJ+sgtuNTtcqavWXqsflWcfBnlYhg/nS4aZ1leewkXGbvBhaapn1q6qf4M71bsR1tez5JTRMuqwA== + dependencies: + "@types/json-schema" "^7.0.3" + "@typescript-eslint/typescript-estree" "2.34.0" + eslint-scope "^5.0.0" + eslint-utils "^2.0.0" + +"@typescript-eslint/typescript-estree@2.34.0": + version "2.34.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-2.34.0.tgz#14aeb6353b39ef0732cc7f1b8285294937cf37d5" + integrity sha512-OMAr+nJWKdlVM9LOqCqh3pQQPwxHAN7Du8DR6dmwCrAmxtiXQnhHJ6tBNtf+cggqfo51SG/FCwnKhXCIM7hnVg== + dependencies: + debug "^4.1.1" + eslint-visitor-keys "^1.1.0" + glob "^7.1.6" + is-glob "^4.0.1" + lodash "^4.17.15" + semver "^7.3.2" + tsutils "^3.17.1" + "@udecode/slate-plugins@^0.60.0": version "0.60.0" resolved "https://registry.yarnpkg.com/@udecode/slate-plugins/-/slate-plugins-0.60.0.tgz#edb05e70d97d8fdb33fbfe26cfd6fdeb10f8e512" @@ -3083,7 +3127,7 @@ configstore@^3.0.0: write-file-atomic "^2.0.0" xdg-basedir "^3.0.0" -confusing-browser-globals@^1.0.5: +confusing-browser-globals@^1.0.9: version "1.0.9" resolved "https://registry.yarnpkg.com/confusing-browser-globals/-/confusing-browser-globals-1.0.9.tgz#72bc13b483c0276801681871d4898516f8f54fdd" integrity sha512-KbS1Y0jMtyPgIxjO7ZzMAuUpAKMt1SzCL9fsrKsX6b0zJPTaT0SiSPmewwVZg9UAO83HVIlEhZF84LIjZ0lmAw== @@ -4125,24 +4169,6 @@ escodegen@^1.11.1: optionalDependencies: source-map "~0.6.1" -eslint-config-airbnb-base@^13.2.0: - version "13.2.0" - resolved "https://registry.yarnpkg.com/eslint-config-airbnb-base/-/eslint-config-airbnb-base-13.2.0.tgz#f6ea81459ff4dec2dda200c35f1d8f7419d57943" - integrity sha512-1mg/7eoB4AUeB0X1c/ho4vb2gYkNH8Trr/EgCT/aGmKhhG+F6vF5s8+iRBlWAzFIAphxIdp3YfEKgEl0f9Xg+w== - dependencies: - confusing-browser-globals "^1.0.5" - object.assign "^4.1.0" - object.entries "^1.1.0" - -eslint-config-airbnb@^17.1.0: - version "17.1.1" - resolved "https://registry.yarnpkg.com/eslint-config-airbnb/-/eslint-config-airbnb-17.1.1.tgz#2272e0b86bb1e2b138cdf88d07a3b6f4cda3d626" - integrity sha512-xCu//8a/aWqagKljt+1/qAM62BYZeNq04HmdevG5yUGWpja0I/xhqd6GdLRch5oetEGFiJAnvtGuTEAese53Qg== - dependencies: - eslint-config-airbnb-base "^13.2.0" - object.assign "^4.1.0" - object.entries "^1.1.0" - eslint-import-resolver-node@^0.3.2: version "0.3.3" resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.3.tgz#dbaa52b6b2816b50bc6711af75422de808e98404" @@ -4151,6 +4177,17 @@ eslint-import-resolver-node@^0.3.2: debug "^2.6.9" resolve "^1.13.1" +eslint-loader@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/eslint-loader/-/eslint-loader-4.0.2.tgz#386a1e21bcb613b3cf2d252a3b708023ccfb41ec" + integrity sha512-EDpXor6lsjtTzZpLUn7KmXs02+nIjGcgees9BYjNkWra3jVq5vVa8IoCKgzT2M7dNNeoMBtaSG83Bd40N3poLw== + dependencies: + find-cache-dir "^3.3.1" + fs-extra "^8.1.0" + loader-utils "^2.0.0" + object-hash "^2.0.3" + schema-utils "^2.6.5" + eslint-module-utils@^2.4.1: version "2.6.0" resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.6.0.tgz#579ebd094f56af7797d19c9866c9c9486629bfa6" @@ -4177,6 +4214,21 @@ eslint-plugin-import@^2.20.0: read-pkg-up "^2.0.0" resolve "^1.12.0" +eslint-plugin-jest-dom@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-jest-dom/-/eslint-plugin-jest-dom-3.0.1.tgz#ab446c015bb7c9faed769533dfc3cecc2fcf6230" + integrity sha512-RszrVljcf+jxfudrvFo469HMVT+yeB5wt/6tY+33Ebiiq7Za1Uh5RVu+ZKPCKSd7E4buyi8bxcHLfNCFXSSz7w== + dependencies: + "@babel/runtime" "^7.9.6" + requireindex "^1.2.0" + +eslint-plugin-jest@^23.16.0: + version "23.16.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-23.16.0.tgz#fada00f765b5e32a1fb10a7490f75ebf673133b3" + integrity sha512-51KcQup31S2NBm+Yqg3rxZIPETd+wZ/gU2rb034RpdXLcZYsa2+uwubqbbDAYIpQw3m+wywF/A56OMEouBY/wA== + dependencies: + "@typescript-eslint/experimental-utils" "^2.5.0" + eslint-plugin-jsx-a11y@^6.2.1: version "6.2.3" resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.2.3.tgz#b872a09d5de51af70a97db1eea7dc933043708aa" @@ -4238,6 +4290,13 @@ eslint-utils@^1.4.3: dependencies: eslint-visitor-keys "^1.1.0" +eslint-utils@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-2.1.0.tgz#d2de5e03424e707dc10c74068ddedae708741b27" + integrity sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg== + dependencies: + eslint-visitor-keys "^1.1.0" + eslint-visitor-keys@^1.0.0, eslint-visitor-keys@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz#e2a82cea84ff246ad6fb57f9bde5b46621459ec2" @@ -4678,7 +4737,7 @@ finalhandler@~1.1.2: statuses "~1.5.0" unpipe "~1.0.0" -find-cache-dir@3.3.1: +find-cache-dir@3.3.1, find-cache-dir@^3.3.1: version "3.3.1" resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-3.3.1.tgz#89b33fad4a4670daa94f855f7fbe31d6d84fe880" integrity sha512-t2GDMt3oGC/v+BMwzmllWDuJF/xcDtE5j/fCGbqDD7OLuJkj0cfh1YSA5VKPvwMeLFLNDBkwOKZ2X85jGLVftQ== @@ -4850,6 +4909,15 @@ fs-constants@^1.0.0: resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== +fs-extra@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" + integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^4.0.0" + universalify "^0.1.0" + fs-minipass@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb" @@ -5012,7 +5080,7 @@ glob-parent@^5.0.0, glob-parent@~5.1.0: dependencies: is-glob "^4.0.1" -glob@^7.0.0, glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@~7.1.1: +glob@^7.0.0, glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@~7.1.1: version "7.1.6" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== @@ -5117,7 +5185,7 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.2.3: resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.3.tgz#4a12ff1b60376ef09862c2093edd908328be8423" integrity sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ== -graceful-fs@^4.1.15: +graceful-fs@^4.1.15, graceful-fs@^4.1.6, graceful-fs@^4.2.0: version "4.2.4" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== @@ -6557,6 +6625,13 @@ json5@^2.1.2: dependencies: minimist "^1.2.5" +jsonfile@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" + integrity sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss= + optionalDependencies: + graceful-fs "^4.1.6" + jsonwebtoken@^8.2.0, jsonwebtoken@^8.5.1: version "8.5.1" resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d" @@ -7779,6 +7854,11 @@ object-copy@^0.1.0: define-property "^0.2.5" kind-of "^3.0.3" +object-hash@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-2.0.3.tgz#d12db044e03cd2ca3d77c0570d87225b02e1e6ea" + integrity sha512-JPKn0GMu+Fa3zt3Bmr66JhokJU5BaNBIh4ZeTlaCBzrBsOeXzwcKKAK1tbLiPKgvwmPXsDvvLHoWh5Bm7ofIYg== + object-inspect@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.7.0.tgz#f4f6bd181ad77f006b5ece60bd0b6f398ff74a67" @@ -7814,7 +7894,7 @@ object.assign@^4.1.0: has-symbols "^1.0.0" object-keys "^1.0.11" -object.entries@^1.1.0, object.entries@^1.1.1: +object.entries@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.1.tgz#ee1cf04153de02bb093fec33683900f57ce5399b" integrity sha512-ilqR7BgdyZetJutmDPfXCDffGa0/Yzl2ivVNpbx/g4UeWrCdRnFDUBrKJGLhGieRHDATnyZXWBeCb29k9CJysQ== @@ -9768,6 +9848,11 @@ require_optional@^1.0.1: resolve-from "^2.0.0" semver "^5.1.0" +requireindex@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/requireindex/-/requireindex-1.2.0.tgz#3463cdb22ee151902635aa6c9535d4de9c2ef1ef" + integrity sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww== + resize-observer-polyfill@^1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464" @@ -10097,7 +10182,7 @@ semver@^6.0.0, semver@^6.1.2, semver@^6.3.0: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== -semver@^7.1.1, semver@^7.1.3: +semver@^7.1.1, semver@^7.1.3, semver@^7.3.2: version "7.3.2" resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.2.tgz#604962b052b81ed0786aae84389ffba70ffd3938" integrity sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ== @@ -11217,7 +11302,7 @@ ts-easing@^0.2.0: resolved "https://registry.yarnpkg.com/ts-easing/-/ts-easing-0.2.0.tgz#c8a8a35025105566588d87dbda05dd7fbfa5a4ec" integrity sha512-Z86EW+fFFh/IFB1fqQ3/+7Zpf9t2ebOAxNI/V6Wo7r5gqiqtxmgTlQ1qbqQcjLKYeSHPTsEmvlJUDg/EuL0uHQ== -tslib@^1.10.0: +tslib@^1.10.0, tslib@^1.8.1: version "1.13.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.13.0.tgz#c881e13cc7015894ed914862d276436fa9a47043" integrity sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q== @@ -11232,6 +11317,13 @@ tslib@^2.0.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.0.0.tgz#18d13fc2dce04051e20f074cc8387fd8089ce4f3" integrity sha512-lTqkx847PI7xEDYJntxZH89L2/aXInsyF2luSafe/+0fHOMjlBNXdH6th7f70qxLDhul7KZK0zC8V5ZIyHl0/g== +tsutils@^3.17.1: + version "3.17.1" + resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.17.1.tgz#ed719917f11ca0dee586272b2ac49e015a2dd759" + integrity sha512-kzeQ5B8H3w60nFY2g8cJIuH7JDpsALXySGtwGJ0p2LSjLgay3NdIpqq5SoOBe46bKDW2iq25irHCr8wjomUS2g== + dependencies: + tslib "^1.8.1" + tty-browserify@0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6" @@ -11412,6 +11504,11 @@ universal-cookie@^3.1.0: cookie "^0.3.1" object-assign "^4.1.0" +universalify@^0.1.0: + version "0.1.2" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" + integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== + unpipe@1.0.0, unpipe@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" From 1e00508d6ad11d736b73f790b66afe2e9bf27bc0 Mon Sep 17 00:00:00 2001 From: Jacob Fletcher Date: Mon, 22 Jun 2020 11:39:31 -0400 Subject: [PATCH 002/125] swaps @trbl react modules for @faceless-ui scope --- package.json | 8 +-- src/client/components/data/User.js | 4 +- .../elements/DeleteDocument/index.js | 2 +- src/client/components/elements/Popup/index.js | 7 +- src/client/components/index.js | 6 +- .../components/modals/StayLoggedIn/index.js | 2 +- yarn.lock | 69 ++++++++++--------- 7 files changed, 49 insertions(+), 49 deletions(-) diff --git a/package.json b/package.json index c5f5715ef..4bfa3c6a8 100644 --- a/package.json +++ b/package.json @@ -25,10 +25,10 @@ "@babel/preset-react": "^7.8.3", "@babel/runtime": "^7.8.4", "@date-io/date-fns": "^1.3.13", - "@trbl/react-collapsibles": "^0.1.0", - "@trbl/react-modal": "^1.0.4", - "@trbl/react-scroll-info": "^1.1.1", - "@trbl/react-window-info": "^1.2.2", + "@faceless-ui/collapsibles": "^0.1.0", + "@faceless-ui/modal": "^1.0.4", + "@faceless-ui/scroll-info": "^1.1.1", + "@faceless-ui/window-info": "^1.2.2", "@udecode/slate-plugins": "^0.60.0", "async-some": "^1.0.2", "babel-loader": "^8.0.6", diff --git a/src/client/components/data/User.js b/src/client/components/data/User.js index 24a8ad4be..42a56a12a 100644 --- a/src/client/components/data/User.js +++ b/src/client/components/data/User.js @@ -6,7 +6,7 @@ import jwt from 'jsonwebtoken'; import PropTypes from 'prop-types'; import Cookies from 'universal-cookie'; import config from 'payload/config'; -import { useModal } from '@trbl/react-modal'; +import { useModal } from '@faceless-ui/modal'; import { requests } from '../../api'; import StayLoggedInModal from '../modals/StayLoggedIn'; import useDebounce from '../../hooks/useDebounce'; @@ -28,7 +28,7 @@ const cookieTokenName = `${cookiePrefix}-token`; const cookies = new Cookies(); const Context = createContext({}); -const isNotExpired = decodedJWT => (decodedJWT?.exp || 0) > Date.now() / 1000; +const isNotExpired = (decodedJWT) => (decodedJWT?.exp || 0) > Date.now() / 1000; const UserProvider = ({ children }) => { const [token, setToken] = useState(''); diff --git a/src/client/components/elements/DeleteDocument/index.js b/src/client/components/elements/DeleteDocument/index.js index 7c8085dc1..845228051 100644 --- a/src/client/components/elements/DeleteDocument/index.js +++ b/src/client/components/elements/DeleteDocument/index.js @@ -2,7 +2,7 @@ import React, { useState, useCallback } from 'react'; import PropTypes from 'prop-types'; import config from 'payload/config'; import { useHistory } from 'react-router-dom'; -import { Modal, useModal } from '@trbl/react-modal'; +import { Modal, useModal } from '@faceless-ui/modal'; import Button from '../Button'; import MinimalTemplate from '../../templates/Minimal'; import useTitle from '../../../hooks/useTitle'; diff --git a/src/client/components/elements/Popup/index.js b/src/client/components/elements/Popup/index.js index 72ca2f8de..cfd527b01 100644 --- a/src/client/components/elements/Popup/index.js +++ b/src/client/components/elements/Popup/index.js @@ -1,7 +1,7 @@ import React, { useEffect, useRef, useState } from 'react'; import PropTypes from 'prop-types'; -import { useWindowInfo } from '@trbl/react-window-info'; -import { useScrollInfo } from '@trbl/react-scroll-info'; +import { useWindowInfo } from '@faceless-ui/window-info'; +import { useScrollInfo } from '@faceless-ui/scroll-info'; import useThrottledEffect from '../../../hooks/useThrottledEffect'; import './index.scss'; @@ -113,8 +113,7 @@ const Popup = (props) => { setActive={setActive} active={active} /> - ) - } + )}
Date: Fri, 3 Jul 2020 10:37:05 -0400 Subject: [PATCH 003/125] fixes bug with field read policy --- src/fields/performFieldOperations.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/fields/performFieldOperations.js b/src/fields/performFieldOperations.js index e6ba168c1..d6a9aae5f 100644 --- a/src/fields/performFieldOperations.js +++ b/src/fields/performFieldOperations.js @@ -35,12 +35,10 @@ module.exports = async (config, operation) => { if (field.policies && field.policies[operationName]) { const result = await field.policies[operationName](operation); - if (!result && operationName === 'create') { - delete resultingData[field.name]; - } - if (!result && operationName === 'update' && originalDoc[field.name] !== undefined) { resultingData[field.name] = originalDoc[field.name]; + } else if (!result) { + delete resultingData[field.name]; } } }; From 2bd5318eb32f8f5ec4a8a143a6b383d5dbe4b26e Mon Sep 17 00:00:00 2001 From: James Date: Fri, 3 Jul 2020 13:08:17 -0400 Subject: [PATCH 004/125] converts cookies to httpOnly server side for security --- src/auth/auth.spec.js | 2 +- src/auth/graphql/resolvers/refresh.js | 4 +- src/auth/operations/login.js | 6 +- src/auth/operations/refresh.js | 10 ++- src/auth/requestHandlers/index.js | 2 + src/auth/requestHandlers/login.js | 1 + src/auth/requestHandlers/logout.js | 11 +++ src/auth/requestHandlers/me.js | 28 +++++- src/auth/requestHandlers/refresh.js | 4 +- src/auth/routes.js | 5 ++ src/client/api.js | 20 +---- src/client/components/Routes.js | 4 + src/client/components/data/User.js | 90 +++++++++---------- src/client/components/forms/Form/index.js | 4 +- .../forms/field-types/Relationship/index.js | 14 +-- .../components/modals/StayLoggedIn/index.js | 6 +- .../components/views/CreateFirstUser/index.js | 2 +- src/express/middleware/index.js | 4 +- src/index.js | 2 - 19 files changed, 120 insertions(+), 99 deletions(-) create mode 100644 src/auth/requestHandlers/logout.js diff --git a/src/auth/auth.spec.js b/src/auth/auth.spec.js index 6c4a700de..c894346bf 100644 --- a/src/auth/auth.spec.js +++ b/src/auth/auth.spec.js @@ -72,7 +72,7 @@ describe('Users REST API', () => { const data = await response.json(); expect(response.status).toBe(200); - expect(data.token).not.toBeNull(); + expect(data.refreshedToken).toBeDefined(); token = data.refreshedToken; }); diff --git a/src/auth/graphql/resolvers/refresh.js b/src/auth/graphql/resolvers/refresh.js index cdea2b3a0..b74733db3 100644 --- a/src/auth/graphql/resolvers/refresh.js +++ b/src/auth/graphql/resolvers/refresh.js @@ -9,9 +9,9 @@ const refreshResolver = (config, collection) => async (_, __, context) => { req: context, }; - const refreshedToken = await refresh(options); + const result = await refresh(options); - return refreshedToken; + return result; }; module.exports = refreshResolver; diff --git a/src/auth/operations/login.js b/src/auth/operations/login.js index 473590442..02d6f1893 100644 --- a/src/auth/operations/login.js +++ b/src/auth/operations/login.js @@ -1,5 +1,5 @@ const jwt = require('jsonwebtoken'); -const { Unauthorized, AuthenticationError } = require('../../errors'); +const { AuthenticationError } = require('../../errors'); const login = async (args) => { try { @@ -65,6 +65,10 @@ const login = async (args) => { }, ); + if (args.res) { + args.res.cookie(`${config.cookiePrefix}-token`, token, { path: '/', httpOnly: true }); + } + // ///////////////////////////////////// // 3. Execute after login hook // ///////////////////////////////////// diff --git a/src/auth/operations/refresh.js b/src/auth/operations/refresh.js index d00ecf30f..6f355c688 100644 --- a/src/auth/operations/refresh.js +++ b/src/auth/operations/refresh.js @@ -1,4 +1,5 @@ const jwt = require('jsonwebtoken'); +const { Forbidden } = require('../../errors'); const refresh = async (args) => { try { @@ -24,6 +25,8 @@ const refresh = async (args) => { const opts = {}; opts.expiresIn = options.collection.config.auth.tokenExpiration; + if (typeof options.authorization !== 'string') throw new Forbidden(); + const token = options.authorization.replace('JWT ', ''); const payload = jwt.verify(token, secret, {}); delete payload.iat; @@ -44,7 +47,12 @@ const refresh = async (args) => { // 4. Return refreshed token // ///////////////////////////////////// - return refreshedToken; + payload.exp = jwt.decode(refreshedToken).exp; + + return { + refreshedToken, + user: payload, + }; } catch (error) { throw error; } diff --git a/src/auth/requestHandlers/index.js b/src/auth/requestHandlers/index.js index d676dcb1e..ceaee4312 100644 --- a/src/auth/requestHandlers/index.js +++ b/src/auth/requestHandlers/index.js @@ -8,9 +8,11 @@ const resetPassword = require('./resetPassword'); const registerFirstUser = require('./registerFirstUser'); const update = require('./update'); const policies = require('./policies'); +const logout = require('./logout'); module.exports = { login, + logout, me, refresh, init, diff --git a/src/auth/requestHandlers/login.js b/src/auth/requestHandlers/login.js index bbaea5226..8d8bb8ba2 100644 --- a/src/auth/requestHandlers/login.js +++ b/src/auth/requestHandlers/login.js @@ -6,6 +6,7 @@ const loginHandler = config => async (req, res) => { try { const token = await login({ req, + res, collection: req.collection, config, data: req.body, diff --git a/src/auth/requestHandlers/logout.js b/src/auth/requestHandlers/logout.js new file mode 100644 index 000000000..161b95dc9 --- /dev/null +++ b/src/auth/requestHandlers/logout.js @@ -0,0 +1,11 @@ +const logoutHandler = config => async (req, res) => { + res.cookie(`${config.cookiePrefix}-token`, '', { + expires: new Date(0), httpOnly: true, path: '/', overwrite: true, + }); + + return res.status(200).json({ + message: 'Logged out successfully.', + }); +}; + +module.exports = logoutHandler; diff --git a/src/auth/requestHandlers/me.js b/src/auth/requestHandlers/me.js index c461d38b3..223ac84da 100644 --- a/src/auth/requestHandlers/me.js +++ b/src/auth/requestHandlers/me.js @@ -1,5 +1,29 @@ -const meHandler = async (req, res) => { - return res.status(200).json(req.user); +const jwt = require('jsonwebtoken'); + +const meHandler = async (req, res, next) => { + try { + if (req.user) { + const response = req.user; + + if (req.headers.authorization && req.headers.authorization.indexOf('JWT') === 0) { + const token = req.headers.authorization.replace('JWT ', ''); + if (token) { + const decoded = jwt.decode(token); + + if (decoded.exp) { + response.exp = decoded.exp; + } + } + } + + return res.status(200).json(response); + } + return res.status(200).json(null); + } catch (err) { + next(err); + } + + return next(); }; module.exports = meHandler; diff --git a/src/auth/requestHandlers/refresh.js b/src/auth/requestHandlers/refresh.js index bf6d6c619..6fa1980db 100644 --- a/src/auth/requestHandlers/refresh.js +++ b/src/auth/requestHandlers/refresh.js @@ -4,7 +4,7 @@ const { refresh } = require('../operations'); const refreshHandler = config => async (req, res) => { try { - const refreshedToken = await refresh({ + const result = await refresh({ req, collection: req.collection, config, @@ -13,7 +13,7 @@ const refreshHandler = config => async (req, res) => { return res.status(200).json({ message: 'Token refresh successful', - refreshedToken, + ...result, }); } catch (error) { return res.status(error.status || httpStatus.INTERNAL_SERVER_ERROR).json(formatErrorResponse(error)); diff --git a/src/auth/routes.js b/src/auth/routes.js index 8ac7edae5..e051a8f25 100644 --- a/src/auth/routes.js +++ b/src/auth/routes.js @@ -4,6 +4,7 @@ const bindCollectionMiddleware = require('../collections/bindCollection'); const { init, login, + logout, refresh, me, register, @@ -35,6 +36,10 @@ const authRoutes = (collection, config, sendEmail) => { .route(`/${slug}/login`) .post(login(config)); + router + .route(`/${slug}/logout`) + .get(logout(config)); + router .route(`/${slug}/refresh-token`) .post(refresh(config)); diff --git a/src/client/api.js b/src/client/api.js index 2813eb782..d57b6cb56 100644 --- a/src/client/api.js +++ b/src/client/api.js @@ -1,24 +1,9 @@ -import Cookies from 'universal-cookie'; import qs from 'qs'; -import config from 'payload/config'; - -const { cookiePrefix } = config; -const cookieTokenName = `${cookiePrefix}-token`; - -export const getJWTHeader = () => { - const cookies = new Cookies(); - const jwt = cookies.get(cookieTokenName); - return jwt ? { Authorization: `JWT ${jwt}` } : {}; -}; export const requests = { get: (url, params) => { const query = qs.stringify(params, { addQueryPrefix: true, depth: 10 }); - return fetch(`${url}${query}`, { - headers: { - ...getJWTHeader(), - }, - }); + return fetch(`${url}${query}`); }, post: (url, options = {}) => { @@ -29,7 +14,6 @@ export const requests = { method: 'post', headers: { ...headers, - ...getJWTHeader(), }, }; @@ -44,7 +28,6 @@ export const requests = { method: 'put', headers: { ...headers, - ...getJWTHeader(), }, }; @@ -58,7 +41,6 @@ export const requests = { method: 'delete', headers: { ...headers, - ...getJWTHeader(), }, }); }, diff --git a/src/client/components/Routes.js b/src/client/components/Routes.js index 027fb89b3..1a95546d5 100644 --- a/src/client/components/Routes.js +++ b/src/client/components/Routes.js @@ -189,6 +189,10 @@ const Routes = () => { return ; } + + if (user === undefined) { + return ; + } return ; }} /> diff --git a/src/client/components/data/User.js b/src/client/components/data/User.js index 24a8ad4be..8b7da70f5 100644 --- a/src/client/components/data/User.js +++ b/src/client/components/data/User.js @@ -1,10 +1,9 @@ import React, { useState, createContext, useContext, useEffect, useCallback, } from 'react'; -import { useLocation, useHistory } from 'react-router-dom'; import jwt from 'jsonwebtoken'; +import { useLocation, useHistory } from 'react-router-dom'; import PropTypes from 'prop-types'; -import Cookies from 'universal-cookie'; import config from 'payload/config'; import { useModal } from '@trbl/react-modal'; import { requests } from '../../api'; @@ -12,7 +11,6 @@ import StayLoggedInModal from '../modals/StayLoggedIn'; import useDebounce from '../../hooks/useDebounce'; const { - cookiePrefix, admin: { user: userSlug, }, @@ -23,16 +21,12 @@ const { }, } = config; -const cookieTokenName = `${cookiePrefix}-token`; - -const cookies = new Cookies(); const Context = createContext({}); -const isNotExpired = decodedJWT => (decodedJWT?.exp || 0) > Date.now() / 1000; - const UserProvider = ({ children }) => { - const [token, setToken] = useState(''); - const [user, setUser] = useState(null); + const [user, setUser] = useState(undefined); + const [tokenInMemory, setTokenInMemory] = useState(null); + const exp = user?.exp; const [permissions, setPermissions] = useState({ canAccessAdmin: null }); @@ -42,60 +36,56 @@ const UserProvider = ({ children }) => { const [lastLocationChange, setLastLocationChange] = useState(0); const debouncedLocationChange = useDebounce(lastLocationChange, 10000); - const exp = user?.exp || 0; const email = user?.email; - const refreshToken = useCallback(() => { - // Need to retrieve token straight from cookie so as to keep this function - // with no dependencies and to make sure we have the exact token that will be used - // in the request to the /refresh route - const tokenFromCookie = cookies.get(cookieTokenName); - const decodedToken = jwt.decode(tokenFromCookie); + const refreshCookie = useCallback(() => { + setTimeout(async () => { + const request = await requests.post(`${serverURL}${api}/${userSlug}/refresh-token`); - if (decodedToken?.exp > (Date.now() / 1000)) { - setTimeout(async () => { - const request = await requests.post(`${serverURL}${api}/${userSlug}/refresh-token`); + if (request.status === 200) { + const json = await request.json(); + setUser(json.user); + } + }, 1000); + }, [setUser]); - if (request.status === 200) { - const json = await request.json(); - setToken(json.refreshedToken); - } - }, 1000); - } - }, [setToken]); + const setToken = useCallback((token) => { + const decoded = jwt.decode(token); + setUser(decoded); + setTokenInMemory(token); + }, []); const logOut = () => { setUser(null); - setToken(null); - cookies.remove(cookieTokenName, { path: '/' }); + setTokenInMemory(null); + requests.get(`${serverURL}${api}/${userSlug}/logout`); }; - // On mount, get cookie and set as token + // On mount, get user and set useEffect(() => { - const cookieToken = cookies.get(cookieTokenName); - if (cookieToken) setToken(cookieToken); + const fetchMe = async () => { + const request = await requests.get(`${serverURL}${api}/${userSlug}/me`); + + if (request.status === 200) { + const json = await request.json(); + setUser(json); + } + }; + + fetchMe(); }, []); - // When location changes, refresh token + // When location changes, refresh cookie useEffect(() => { - refreshToken(); - }, [debouncedLocationChange, refreshToken]); + if (email) { + refreshCookie(); + } + }, [debouncedLocationChange, refreshCookie, email]); useEffect(() => { setLastLocationChange(Date.now()); }, [pathname]); - // When token changes, set cookie, decode and set user - useEffect(() => { - if (token) { - const decoded = jwt.decode(token); - if (isNotExpired(decoded)) { - setUser(decoded); - cookies.set(cookieTokenName, token, { path: '/' }); - } - } - }, [token]); - // When user changes, get new policies useEffect(() => { async function getPermissions() { @@ -149,15 +139,15 @@ const UserProvider = ({ children }) => { return ( {children} - + ); }; diff --git a/src/client/components/forms/Form/index.js b/src/client/components/forms/Form/index.js index bfa27053d..90a98d4c4 100644 --- a/src/client/components/forms/Form/index.js +++ b/src/client/components/forms/Form/index.js @@ -54,7 +54,7 @@ const Form = (props) => { const history = useHistory(); const locale = useLocale(); const { replaceStatus, addStatus, clearStatus } = useStatusList(); - const { refreshToken } = useUser(); + const { refreshCookie } = useUser(); const contextRef = useRef({ ...initContextState }); @@ -278,7 +278,7 @@ const Form = (props) => { }; useThrottledEffect(() => { - refreshToken(); + refreshCookie(); }, 15000, [fields]); useEffect(() => { diff --git a/src/client/components/forms/field-types/Relationship/index.js b/src/client/components/forms/field-types/Relationship/index.js index 96c9ad31d..0fedc1419 100644 --- a/src/client/components/forms/field-types/Relationship/index.js +++ b/src/client/components/forms/field-types/Relationship/index.js @@ -2,7 +2,6 @@ import React, { Component, useState, useEffect, useCallback, } from 'react'; import PropTypes from 'prop-types'; -import Cookies from 'universal-cookie'; import some from 'async-some'; import config from 'payload/config'; import withCondition from '../../withCondition'; @@ -14,14 +13,10 @@ import { relationship } from '../../../../../fields/validations'; import './index.scss'; -const cookies = new Cookies(); - const { - cookiePrefix, serverURL, routes: { api }, collections, + serverURL, routes: { api }, collections, } = config; -const cookieTokenName = `${cookiePrefix}-token`; - const maxResultsPerRequest = 10; class Relationship extends Component { @@ -62,7 +57,6 @@ class Relationship extends Component { const { relations, lastFullyLoadedRelation, lastLoadedPage, search, } = this.state; - const token = cookies.get(cookieTokenName); const relationsToSearch = relations.slice(lastFullyLoadedRelation + 1); @@ -71,11 +65,7 @@ class Relationship extends Component { const collection = collections.find(coll => coll.slug === relation); const fieldToSearch = collection.useAsTitle || 'id'; const searchParam = search ? `&where[${fieldToSearch}][like]=${search}` : ''; - const response = await fetch(`${serverURL}${api}/${relation}?limit=${maxResultsPerRequest}&page=${lastLoadedPage}${searchParam}`, { - headers: { - Authorization: `JWT ${token}`, - }, - }); + const response = await fetch(`${serverURL}${api}/${relation}?limit=${maxResultsPerRequest}&page=${lastLoadedPage}${searchParam}`); const data = await response.json(); diff --git a/src/client/components/modals/StayLoggedIn/index.js b/src/client/components/modals/StayLoggedIn/index.js index b6f1c9717..2d6ace8cf 100644 --- a/src/client/components/modals/StayLoggedIn/index.js +++ b/src/client/components/modals/StayLoggedIn/index.js @@ -13,7 +13,7 @@ const baseClass = 'stay-logged-in'; const { routes: { admin } } = config; const StayLoggedInModal = (props) => { - const { refreshToken } = props; + const { refreshCookie } = props; const history = useHistory(); const { closeAll: closeAllModals } = useModal(); @@ -36,7 +36,7 @@ const StayLoggedInModal = (props) => { Log out - +
+
+ {!disableColumns && ( + + )} + +
+
- - - + {!disableColumns && ( + + + + )} { ); }; +ListControls.defaultProps = { + disableColumns: false, +}; + ListControls.propTypes = { + disableColumns: PropTypes.bool, collection: PropTypes.shape({ useAsTitle: PropTypes.string, fields: PropTypes.arrayOf(PropTypes.shape), diff --git a/src/client/components/elements/ListControls/index.scss b/src/client/components/elements/ListControls/index.scss index bdef8384f..875893e27 100644 --- a/src/client/components/elements/ListControls/index.scss +++ b/src/client/components/elements/ListControls/index.scss @@ -15,9 +15,20 @@ } } + &__buttons { + margin-left: $baseline; + } + + &__buttons-wrap { + display: flex; + margin-left: - base(.5); + margin-right: - base(.5); + width: calc(100% + #{$baseline}); + } + &__toggle-columns, &__toggle-where { - margin: 0 0 0 $baseline; + margin: 0 base(.5); min-width: 140px; &.btn--style-primary { @@ -33,9 +44,8 @@ } @include mid-break { - &__toggle-columns, - &__toggle-where { - margin: 0 0 0 base(.5); + &__buttons { + margin-left: base(.5); } } @@ -49,17 +59,17 @@ width: 100%; } + &__buttons { + margin: 0; + } + + &__buttons { + width: 100%; + } + &__toggle-columns, &__toggle-where { - width: calc(50% - #{base(.25)}); - } - - &__toggle-columns { - margin: 0 base(.25) 0 0; - } - - &__toggle-where { - margin: 0 0 0 base(.25); + flex-grow: 1; } } } diff --git a/src/client/components/forms/field-types/Upload/SelectExisting/index.js b/src/client/components/forms/field-types/Upload/SelectExisting/index.js index 05fca5896..13ec419d8 100644 --- a/src/client/components/forms/field-types/Upload/SelectExisting/index.js +++ b/src/client/components/forms/field-types/Upload/SelectExisting/index.js @@ -74,6 +74,7 @@ const SelectExistingUploadModal = (props) => { /> Date: Mon, 6 Jul 2020 16:38:06 -0400 Subject: [PATCH 010/125] swaps in UploadGallery in place of Table for collections that have uploads present --- .../views/collections/List/Default.js | 88 +++++++++++-------- .../views/collections/List/index.scss | 1 + 2 files changed, 53 insertions(+), 36 deletions(-) diff --git a/src/client/components/views/collections/List/Default.js b/src/client/components/views/collections/List/Default.js index 86ff2659d..7a4fffa06 100644 --- a/src/client/components/views/collections/List/Default.js +++ b/src/client/components/views/collections/List/Default.js @@ -1,9 +1,10 @@ import React, { useEffect, useState } from 'react'; -import { useLocation } from 'react-router-dom'; +import { useLocation, useHistory } from 'react-router-dom'; import queryString from 'qs'; import PropTypes from 'prop-types'; import config from 'payload/config'; import usePayloadAPI from '../../../../hooks/usePayloadAPI'; +import UploadGallery from '../../../elements/UploadGallery'; import Eyebrow from '../../../elements/Eyebrow'; import Paginator from '../../../elements/Paginator'; import ListControls from '../../../elements/ListControls'; @@ -23,6 +24,7 @@ const DefaultList = (props) => { const { collection, collection: { + upload, fields, slug, labels: { @@ -32,6 +34,7 @@ const DefaultList = (props) => { }, } = props; + const history = useHistory(); const location = useLocation(); const [listControls, setListControls] = useState({}); const [sort, setSort] = useState(null); @@ -66,46 +69,58 @@ const DefaultList = (props) => { {(data.docs && data.docs.length > 0) && ( - { - const field = fields.find(fieldToCheck => fieldToCheck.name === col); + <> + {!upload && ( +
{ + const field = fields.find(fieldToCheck => fieldToCheck.name === col); - if (field) { - return [ - ...cols, - { - accessor: field.name, - components: { - Heading: ( - - ), - renderCell: (rowData, cellData) => { - return ( - - ); + if (field) { + return [ + ...cols, + { + accessor: field.name, + components: { + Heading: ( + + ), + renderCell: (rowData, cellData) => { + return ( + + ); + }, + }, }, - }, - }, - ]; - } + ]; + } - return cols; - }, [])} - /> + return cols; + }, [])} + /> + )} + {upload && ( + history.push(`${admin}/collections/${slug}/${doc.id}`)} + /> + )} + )} {data.docs && data.docs.length === 0 && (
@@ -160,6 +175,7 @@ const DefaultList = (props) => { DefaultList.propTypes = { collection: PropTypes.shape({ + upload: PropTypes.shape({}), labels: PropTypes.shape({ singular: PropTypes.string, plural: PropTypes.string, diff --git a/src/client/components/views/collections/List/index.scss b/src/client/components/views/collections/List/index.scss index 6026e950a..9983c2bf9 100644 --- a/src/client/components/views/collections/List/index.scss +++ b/src/client/components/views/collections/List/index.scss @@ -61,6 +61,7 @@ &__header, .list-controls, .table, + .upload-gallery, &__no-results, &__page-controls { padding-left: $baseline; From a471e259c1c842210fd50e8adfc397fd8f2d3a99 Mon Sep 17 00:00:00 2001 From: James Date: Mon, 6 Jul 2020 17:00:18 -0400 Subject: [PATCH 011/125] improves prop conventions of ListControls --- src/client/components/elements/ListControls/index.js | 10 +++++----- .../forms/field-types/Upload/SelectExisting/index.js | 2 +- .../components/views/collections/List/Default.js | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/client/components/elements/ListControls/index.js b/src/client/components/elements/ListControls/index.js index 1ea4b188f..6e9ea0268 100644 --- a/src/client/components/elements/ListControls/index.js +++ b/src/client/components/elements/ListControls/index.js @@ -14,7 +14,7 @@ const ListControls = (props) => { const { handleChange, collection, - disableColumns, + enableColumns, collection: { fields, useAsTitle, @@ -74,7 +74,7 @@ const ListControls = (props) => { />
- {!disableColumns && ( + {enableColumns && (
- {!disableColumns && ( + {enableColumns && ( { }; ListControls.defaultProps = { - disableColumns: false, + enableColumns: true, }; ListControls.propTypes = { - disableColumns: PropTypes.bool, + enableColumns: PropTypes.bool, collection: PropTypes.shape({ useAsTitle: PropTypes.string, fields: PropTypes.arrayOf(PropTypes.shape), diff --git a/src/client/components/forms/field-types/Upload/SelectExisting/index.js b/src/client/components/forms/field-types/Upload/SelectExisting/index.js index 13ec419d8..a6e2c0ec2 100644 --- a/src/client/components/forms/field-types/Upload/SelectExisting/index.js +++ b/src/client/components/forms/field-types/Upload/SelectExisting/index.js @@ -74,7 +74,7 @@ const SelectExistingUploadModal = (props) => { /> { {(data.docs && data.docs.length > 0) && ( <> From 83567c68dbb933dfe8f698c0cc60e3d92c4fbcf7 Mon Sep 17 00:00:00 2001 From: James Date: Mon, 6 Jul 2020 17:00:33 -0400 Subject: [PATCH 012/125] implements a way to secure token cookies via auth configs --- demo/collections/Admin.js | 1 + demo/collections/PublicUsers.js | 1 + src/auth/operations/login.js | 11 ++++++++++- src/auth/operations/refresh.js | 12 +++++++++++- src/auth/requestHandlers/logout.js | 17 ++++++++++++++--- 5 files changed, 37 insertions(+), 5 deletions(-) diff --git a/demo/collections/Admin.js b/demo/collections/Admin.js index 091429bca..d0b5679b1 100644 --- a/demo/collections/Admin.js +++ b/demo/collections/Admin.js @@ -23,6 +23,7 @@ module.exports = { auth: { tokenExpiration: 300, useAPIKey: true, + secureCookie: process.env.NODE_ENV === 'production', }, fields: [ { diff --git a/demo/collections/PublicUsers.js b/demo/collections/PublicUsers.js index da9a2f374..a13d44d74 100644 --- a/demo/collections/PublicUsers.js +++ b/demo/collections/PublicUsers.js @@ -30,6 +30,7 @@ module.exports = { }, auth: { tokenExpiration: 300, + secureCookie: process.env.NODE_ENV === 'production', }, fields: [ { diff --git a/src/auth/operations/login.js b/src/auth/operations/login.js index 02d6f1893..bd07246bc 100644 --- a/src/auth/operations/login.js +++ b/src/auth/operations/login.js @@ -66,7 +66,16 @@ const login = async (args) => { ); if (args.res) { - args.res.cookie(`${config.cookiePrefix}-token`, token, { path: '/', httpOnly: true }); + const cookieOptions = { + path: '/', + httpOnly: true, + }; + + if (collectionConfig.auth.secureCookie) { + cookieOptions.secure = true; + } + + args.res.cookie(`${config.cookiePrefix}-token`, token, cookieOptions); } // ///////////////////////////////////// diff --git a/src/auth/operations/refresh.js b/src/auth/operations/refresh.js index b00edf130..729efdd68 100644 --- a/src/auth/operations/refresh.js +++ b/src/auth/operations/refresh.js @@ -33,9 +33,19 @@ const refresh = async (args) => { const refreshedToken = jwt.sign(payload, secret, opts); if (args.res) { - args.res.cookie(`${cookiePrefix}-token`, refreshedToken, { path: '/', httpOnly: true }); + const cookieOptions = { + path: '/', + httpOnly: true, + }; + + if (options.collection.config.auth.secureCookie) { + cookieOptions.secure = true; + } + + args.res.cookie(`${cookiePrefix}-token`, refreshedToken, cookieOptions); } + // ///////////////////////////////////// // 3. Execute after login hook // ///////////////////////////////////// diff --git a/src/auth/requestHandlers/logout.js b/src/auth/requestHandlers/logout.js index 161b95dc9..6222ffd25 100644 --- a/src/auth/requestHandlers/logout.js +++ b/src/auth/requestHandlers/logout.js @@ -1,7 +1,18 @@ const logoutHandler = config => async (req, res) => { - res.cookie(`${config.cookiePrefix}-token`, '', { - expires: new Date(0), httpOnly: true, path: '/', overwrite: true, - }); + const { collection } = req; + + const cookieOptions = { + expires: new Date(0), + httpOnly: true, + path: '/', + overwrite: true, + }; + + if (collection.auth && collection.auth.secureCookie) { + cookieOptions.secure = true; + } + + res.cookie(`${config.cookiePrefix}-token`, '', cookieOptions); return res.status(200).json({ message: 'Logged out successfully.', From b263e04408e559e563c295d26a782dd6700fb211 Mon Sep 17 00:00:00 2001 From: James Date: Mon, 6 Jul 2020 22:58:31 -0400 Subject: [PATCH 013/125] WIP - styles Globals --- demo/collections/Admin.js | 2 +- demo/globals/GlobalWithPolicies.js | 1 - src/client/components/views/Global/Default.js | 147 ++++++++++++++ src/client/components/views/Global/index.js | 90 +++++---- src/client/components/views/Global/index.scss | 186 ++++++++++++++++++ .../views/collections/Edit/Default.js | 5 +- src/collections/operations/update.js | 2 +- src/globals/buildModel.js | 15 +- src/globals/init.js | 1 - src/globals/operations/findOne.js | 12 +- src/globals/operations/update.js | 22 +-- src/globals/requestHandlers/findOne.js | 5 +- src/globals/requestHandlers/update.js | 6 +- src/globals/routes.js | 3 +- src/utilities/getPropSubset.js | 1 - src/utilities/secureConfig.js | 33 ---- 16 files changed, 418 insertions(+), 113 deletions(-) create mode 100644 src/client/components/views/Global/Default.js create mode 100644 src/client/components/views/Global/index.scss delete mode 100644 src/utilities/getPropSubset.js delete mode 100644 src/utilities/secureConfig.js diff --git a/demo/collections/Admin.js b/demo/collections/Admin.js index d0b5679b1..4f6b2d631 100644 --- a/demo/collections/Admin.js +++ b/demo/collections/Admin.js @@ -21,7 +21,7 @@ module.exports = { admin: () => true, }, auth: { - tokenExpiration: 300, + tokenExpiration: 7200, useAPIKey: true, secureCookie: process.env.NODE_ENV === 'production', }, diff --git a/demo/globals/GlobalWithPolicies.js b/demo/globals/GlobalWithPolicies.js index a7c721989..ad43109be 100644 --- a/demo/globals/GlobalWithPolicies.js +++ b/demo/globals/GlobalWithPolicies.js @@ -12,7 +12,6 @@ module.exports = { name: 'title', label: 'Site Title', type: 'text', - localized: true, maxLength: 100, required: true, }, diff --git a/src/client/components/views/Global/Default.js b/src/client/components/views/Global/Default.js new file mode 100644 index 000000000..ff61fa946 --- /dev/null +++ b/src/client/components/views/Global/Default.js @@ -0,0 +1,147 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import format from 'date-fns/format'; +import config from 'payload/config'; +import Eyebrow from '../../elements/Eyebrow'; +import Form from '../../forms/Form'; +import PreviewButton from '../../elements/PreviewButton'; +import FormSubmit from '../../forms/Submit'; +import RenderFields from '../../forms/RenderFields'; +import CopyToClipboard from '../../elements/CopyToClipboard'; +import * as fieldTypes from '../../forms/field-types'; +import LeaveWithoutSaving from '../../modals/LeaveWithoutSaving'; + +import './index.scss'; + +const { serverURL, routes: { api } } = config; + +const baseClass = 'global-edit'; + +const DefaultGlobalView = (props) => { + const { + global, data, onSave, permissions, + } = props; + + const { + slug, + fields, + preview, + label, + } = global; + + const apiURL = `${serverURL}${api}/globals/${slug}`; + const action = `${serverURL}${api}/globals/${slug}`; + const hasSavePermission = permissions?.update?.permission; + + return ( +
+
+
+ + +
+
+

+ Edit + {' '} + {label} +

+
+ (!field.position || (field.position && field.position !== 'sidebar'))} + fieldTypes={fieldTypes} + fieldSchema={fields} + initialData={data} + customComponentsPath={`${slug}.fields.`} + /> +
+
+
+
+ + {hasSavePermission && ( + Save + )} +
+ {data && ( +
+ + API URL + {' '} + + + + {apiURL} + +
+ )} +
+ field.position === 'sidebar'} + position="sidebar" + fieldTypes={fieldTypes} + fieldSchema={fields} + initialData={data} + customComponentsPath={`${slug}.fields.`} + /> +
+ {data && ( +
    + {data && ( + <> + {data.updatedAt && ( +
  • +
    Last Modified
    +
    {format(new Date(data.updatedAt), 'MMMM do yyyy, h:mma')}
    +
  • + )} + + )} +
+ )} +
+ +
+ ); +}; + +DefaultGlobalView.defaultProps = { + data: undefined, +}; + +DefaultGlobalView.propTypes = { + global: PropTypes.shape({ + label: PropTypes.string.isRequired, + slug: PropTypes.string, + fields: PropTypes.arrayOf(PropTypes.shape({})), + preview: PropTypes.func, + }).isRequired, + data: PropTypes.shape({ + updatedAt: PropTypes.string, + }), + onSave: PropTypes.func.isRequired, + permissions: PropTypes.shape({ + update: PropTypes.shape({ + permission: PropTypes.bool, + }), + fields: PropTypes.shape({}), + }).isRequired, +}; + +export default DefaultGlobalView; diff --git a/src/client/components/views/Global/index.js b/src/client/components/views/Global/index.js index 5c372647d..73a1bcdff 100644 --- a/src/client/components/views/Global/index.js +++ b/src/client/components/views/Global/index.js @@ -1,68 +1,76 @@ import React, { useEffect } from 'react'; import PropTypes from 'prop-types'; +import { useHistory, useLocation } from 'react-router-dom'; import config from 'payload/config'; import { useStepNav } from '../../elements/StepNav'; import usePayloadAPI from '../../../hooks/usePayloadAPI'; -import Form from '../../forms/Form'; -import RenderFields from '../../forms/RenderFields'; -import * as fieldTypes from '../../forms/field-types'; +import { useUser } from '../../data/User'; -const { - serverURL, - routes: { - admin, - api, - }, -} = config; +import RenderCustomComponent from '../../utilities/RenderCustomComponent'; +import DefaultGlobal from './Default'; -const baseClass = 'global-edit'; +const { serverURL, routes: { admin, api } } = config; -const Global = (props) => { - const { global: { slug, label, fields } } = props; +const GlobalView = (props) => { + const { state: locationState } = useLocation(); + const history = useHistory(); const { setStepNav } = useStepNav(); + const { permissions } = useUser(); + + const { global } = props; + + const { + slug, + label, + } = global; + + const onSave = (json) => { + history.push(`${admin}/globals/${global.slug}`, { + status: { + message: json.message, + type: 'success', + }, + data: json.doc, + }); + }; const [{ data }] = usePayloadAPI( `${serverURL}${api}/globals/${slug}`, { initialParams: { 'fallback-locale': 'null' } }, ); + const dataToRender = locationState?.data || data; + useEffect(() => { - setStepNav([{ - url: `${admin}/globals/${slug}`, + const nav = [{ label, - }]); - }, [setStepNav, slug, label]); + }]; + + setStepNav(nav); + }, [setStepNav, label]); + + const globalPermissions = permissions?.[slug]; return ( -
-
-

- Edit - {' '} - {label} -

-
-
- - -
+ ); }; -Global.propTypes = { +GlobalView.propTypes = { global: PropTypes.shape({ - label: PropTypes.string, - slug: PropTypes.string, + label: PropTypes.string.isRequired, + slug: PropTypes.string.isRequired, fields: PropTypes.arrayOf(PropTypes.shape({})), }).isRequired, }; -export default Global; +export default GlobalView; diff --git a/src/client/components/views/Global/index.scss b/src/client/components/views/Global/index.scss new file mode 100644 index 000000000..495816a66 --- /dev/null +++ b/src/client/components/views/Global/index.scss @@ -0,0 +1,186 @@ +@import '../../../scss/styles.scss'; + +.global-edit { + width: 100%; + + &__form { + display: flex; + align-items: stretch; + } + + &__main { + min-width: 0; + } + + &__header { + h1 { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + } + + &__collection-actions { + list-style: none; + margin: 0; + padding: base(1.5) 0 base(.5); + display: flex; + + li { + margin-right: base(.75); + } + } + + &__edit { + background: white; + padding: base(5) base(6) base(6); + } + + &__sidebar { + padding-top: base(3); + padding-bottom: base(1); + width: base(22); + display: flex; + flex-direction: column; + min-width: 0; + } + + &__collection-actions, + &__document-actions, + &__meta, + &__sidebar-fields, + &__api-url { + padding-left: base(1.5); + } + + &__document-actions { + @include blur-bg; + position: sticky; + top: 0; + z-index: 2; + padding-right: $baseline; + } + + &__document-actions--with-preview { + display: flex; + + > * { + width: calc(50% - #{base(.5)}); + } + + > *:first-child { + margin-right: base(.5); + } + + > *:last-child { + margin-left: base(.5); + } + + .form-submit { + .btn { + width: 100%; + padding-left: base(2); + padding-right: base(2); + } + } + } + + &__api-url { + margin-bottom: base(1.5); + padding-right: base(1.5); + + a { + display: block; + overflow: hidden; + text-overflow: ellipsis; + } + } + + &__meta { + margin: 0; + padding-top: $baseline; + list-style: none; + + li { + margin-bottom: base(.5); + } + } + + &__label { + color: $color-gray; + } + + &__collection-actions, + &__api-url { + a, button { + cursor: pointer; + font-weight: 600; + text-decoration: none; + + &:hover { + text-decoration: underline; + } + } + } + + &--is-editing { + .collection-edit__sidebar { + padding-top: 0; + } + } + + @include large-break { + &__edit { + padding: base(3.5); + } + } + + @include mid-break { + &__sidebar { + width: unset; + } + + &__form { + display: block; + } + + &__edit { + padding: $baseline; + margin-bottom: $baseline; + } + + &__document-actions { + position: fixed; + bottom: 0; + left: 0; + right: 0; + top: auto; + } + + &__document-actions, + &__meta, + &__sidebar-fields, + &__api-url { + padding-left: $baseline; + } + + &__api-url { + margin-bottom: base(.5); + } + + &__collection-actions { + margin-top: base(.5); + padding-left: $baseline; + padding-bottom: 0; + order: 1; + + li { + margin: 0 base(.5) 0 0; + } + } + + &__sidebar { + padding-bottom: base(4); + } + } +} diff --git a/src/client/components/views/collections/Edit/Default.js b/src/client/components/views/collections/Edit/Default.js index d863c9934..7ebdcc01f 100644 --- a/src/client/components/views/collections/Edit/Default.js +++ b/src/client/components/views/collections/Edit/Default.js @@ -39,7 +39,7 @@ const DefaultEditView = (props) => { const apiURL = `${serverURL}${api}/${slug}/${id}`; let action = `${serverURL}${api}/${slug}${isEditing ? `/${id}` : ''}`; - const hasSavePermission = (isEditing && permissions?.update?.permission) || (!isEditing && permissions?.update?.permission); + const hasSavePermission = (isEditing && permissions?.update?.permission) || (!isEditing && permissions?.create?.permission); if (auth && !isEditing) { action = `${action}/register`; @@ -170,7 +170,6 @@ const DefaultEditView = (props) => { DefaultEditView.defaultProps = { isEditing: false, data: undefined, - onSave: null, }; DefaultEditView.propTypes = { @@ -191,7 +190,7 @@ DefaultEditView.propTypes = { updatedAt: PropTypes.string, createdAt: PropTypes.string, }), - onSave: PropTypes.func, + onSave: PropTypes.func.isRequired, permissions: PropTypes.shape({ create: PropTypes.shape({ permission: PropTypes.bool, diff --git a/src/collections/operations/update.js b/src/collections/operations/update.js index abe5be00c..eed1b3187 100644 --- a/src/collections/operations/update.js +++ b/src/collections/operations/update.js @@ -154,7 +154,7 @@ const update = async (args) => { } // ///////////////////////////////////// - // 7. Return updated document + // 9. Return updated document // ///////////////////////////////////// return doc; diff --git a/src/globals/buildModel.js b/src/globals/buildModel.js index ae6e69e30..6b6ade840 100644 --- a/src/globals/buildModel.js +++ b/src/globals/buildModel.js @@ -1,6 +1,11 @@ const mongoose = require('mongoose'); const autopopulate = require('mongoose-autopopulate'); -const mongooseHidden = require('mongoose-hidden'); +const mongooseHidden = require('mongoose-hidden')({ + hidden: { + _id: true, __v: true, + }, + applyRecursively: true, +}); const buildSchema = require('../mongoose/buildSchema'); const localizationPlugin = require('../localization/plugin'); @@ -9,18 +14,12 @@ const buildModel = (config) => { const globalsSchema = new mongoose.Schema({}, { discriminatorKey: 'globalType', timestamps: false }) .plugin(localizationPlugin, config.localization) .plugin(autopopulate) - .plugin(mongooseHidden()); + .plugin(mongooseHidden); const Globals = mongoose.model('globals', globalsSchema); Object.values(config.globals).forEach((globalConfig) => { const globalSchema = buildSchema(globalConfig.fields); - - globalSchema - .plugin(localizationPlugin, config.localization) - .plugin(autopopulate) - .plugin(mongooseHidden()); - Globals.discriminator(globalConfig.slug, globalSchema); }); diff --git a/src/globals/init.js b/src/globals/init.js index 397cde784..a1bd49218 100644 --- a/src/globals/init.js +++ b/src/globals/init.js @@ -1,5 +1,4 @@ const buildModel = require('./buildModel'); -const sanitize = require('./sanitize'); const routes = require('./routes'); function initGlobals() { diff --git a/src/globals/operations/findOne.js b/src/globals/operations/findOne.js index 5cd38360f..e2f1630de 100644 --- a/src/globals/operations/findOne.js +++ b/src/globals/operations/findOne.js @@ -55,14 +55,18 @@ const findOne = async (args) => { } let result = await Model.findOne({ globalType: slug }); + let data = {}; - if (!result) throw new NotFound(); + if (!result) { + result = {}; + } else { + if (locale && result.setLocale) { + result.setLocale(locale, fallbackLocale); + } - if (locale && result.setLocale) { - result.setLocale(locale, fallbackLocale); + data = result.toJSON({ virtuals: true }); } - let data = result.toJSON({ virtuals: true }); // ///////////////////////////////////// // 4. Execute field-level hooks and policies diff --git a/src/globals/operations/update.js b/src/globals/operations/update.js index 17326245a..c63bacaa8 100644 --- a/src/globals/operations/update.js +++ b/src/globals/operations/update.js @@ -29,7 +29,7 @@ const update = async (args) => { let global = await Model.findOne({ globalType: slug }); if (!global) { - global = new Model(); + global = new Model({ globalType: slug }); } if (locale && global.setLocale) { @@ -39,7 +39,7 @@ const update = async (args) => { const globalJSON = global.toJSON({ virtuals: true }); // ///////////////////////////////////// - // 2. Execute before global hook + // 3. Execute before global hook // ///////////////////////////////////// const { beforeUpdate } = args.config.hooks; @@ -49,37 +49,37 @@ const update = async (args) => { } // ///////////////////////////////////// - // 3. Merge updates into existing data + // 4. Merge updates into existing data // ///////////////////////////////////// options.data = deepmerge(globalJSON, options.data, { arrayMerge: overwriteMerge }); // ///////////////////////////////////// - // 4. Execute field-level hooks, policies, and validation + // 5. Execute field-level hooks, policies, and validation // ///////////////////////////////////// options.data = await performFieldOperations(args.config, { ...options, hook: 'beforeUpdate', operationName: 'update' }); // ///////////////////////////////////// - // 4. Perform database operation + // 6. Perform database operation // ///////////////////////////////////// - Object.assign(global, { ...options.data, globalType: slug }); + Object.assign(global, options.data); - global.save(); + await global.save(); global = global.toJSON({ virtuals: true }); // ///////////////////////////////////// - // 5. Execute field-level hooks and policies + // 7. Execute field-level hooks and policies // ///////////////////////////////////// - global = performFieldOperations(args.config, { + global = await performFieldOperations(args.config, { ...options, data: global, hook: 'afterRead', operationName: 'read', }); // ///////////////////////////////////// - // 6. Execute after global hook + // 8. Execute after global hook // ///////////////////////////////////// const { afterUpdate } = args.config.hooks; @@ -89,7 +89,7 @@ const update = async (args) => { } // ///////////////////////////////////// - // 7. Return global + // 9. Return global // ///////////////////////////////////// return global; diff --git a/src/globals/requestHandlers/findOne.js b/src/globals/requestHandlers/findOne.js index 932ae2456..2b6fb6ee1 100644 --- a/src/globals/requestHandlers/findOne.js +++ b/src/globals/requestHandlers/findOne.js @@ -1,8 +1,7 @@ const httpStatus = require('http-status'); -const formatErrorResponse = require('../../express/responses/formatError'); const { findOne } = require('../operations'); -const findOneHandler = (Model, config) => async (req, res) => { +const findOneHandler = (Model, config) => async (req, res, next) => { try { const { slug } = config; @@ -16,7 +15,7 @@ const findOneHandler = (Model, config) => async (req, res) => { return res.status(httpStatus.OK).json(result); } catch (error) { - return res.status(error.status || httpStatus.INTERNAL_SERVER_ERROR).json(formatErrorResponse(error)); + return next(error); } }; diff --git a/src/globals/requestHandlers/update.js b/src/globals/requestHandlers/update.js index 675a7b67e..f40b455a7 100644 --- a/src/globals/requestHandlers/update.js +++ b/src/globals/requestHandlers/update.js @@ -1,8 +1,7 @@ const httpStatus = require('http-status'); -const formatErrorResponse = require('../../express/responses/formatError'); const { update } = require('../operations'); -const updateHandler = (Model, config) => async (req, res) => { +const updateHandler = (Model, config) => async (req, res, next) => { try { const { slug } = config; @@ -12,11 +11,12 @@ const updateHandler = (Model, config) => async (req, res) => { config, slug, depth: req.query.depth, + data: req.body, }); return res.status(httpStatus.OK).json({ message: 'Global saved successfully.', result }); } catch (error) { - return res.status(error.status || httpStatus.INTERNAL_SERVER_ERROR).json(formatErrorResponse(error)); + return next(error); } }; diff --git a/src/globals/routes.js b/src/globals/routes.js index c242f8c80..ec2016bac 100644 --- a/src/globals/routes.js +++ b/src/globals/routes.js @@ -10,8 +10,7 @@ const registerGlobals = (globalConfigs, Globals) => { router .route(`/globals/${global.slug}`) .get(findOne(Globals, global)) - .post(update(Globals, global)) - .put(update(Globals, global)); + .post(update(Globals, global)); }); return router; diff --git a/src/utilities/getPropSubset.js b/src/utilities/getPropSubset.js deleted file mode 100644 index 07635e965..000000000 --- a/src/utilities/getPropSubset.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = (keys, obj) => keys.reduce((a, c) => ({ ...a, [c]: obj[c] }), {}); diff --git a/src/utilities/secureConfig.js b/src/utilities/secureConfig.js deleted file mode 100644 index b0f4b0d4d..000000000 --- a/src/utilities/secureConfig.js +++ /dev/null @@ -1,33 +0,0 @@ -const deepCopyObject = require('../utilities/deepCopyObject'); - -const recursivelySecure = (object) => { - const newObject = deepCopyObject(object); - - delete newObject.hooks; - delete newObject.components; - delete newObject.policies; - - if (newObject.fields) { - newObject.fields.forEach((field, i) => { - newObject.fields[i] = recursivelySecure(field); - }); - } - - return newObject; -}; - -const secureConfig = (insecureConfig) => { - const config = deepCopyObject(insecureConfig); - - delete config.secret; - - recursivelySecure(config); - - config.collections.forEach((collection, i) => { - config.collections[i] = recursivelySecure(collection); - }); - - return config; -}; - -module.exports = secureConfig; From ae35bd7266b3613492eba5e7ac56c020bf497797 Mon Sep 17 00:00:00 2001 From: James Date: Mon, 6 Jul 2020 23:10:50 -0400 Subject: [PATCH 014/125] fixes broken global virtuals --- src/globals/buildModel.js | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/globals/buildModel.js b/src/globals/buildModel.js index 6b6ade840..ae6e69e30 100644 --- a/src/globals/buildModel.js +++ b/src/globals/buildModel.js @@ -1,11 +1,6 @@ const mongoose = require('mongoose'); const autopopulate = require('mongoose-autopopulate'); -const mongooseHidden = require('mongoose-hidden')({ - hidden: { - _id: true, __v: true, - }, - applyRecursively: true, -}); +const mongooseHidden = require('mongoose-hidden'); const buildSchema = require('../mongoose/buildSchema'); const localizationPlugin = require('../localization/plugin'); @@ -14,12 +9,18 @@ const buildModel = (config) => { const globalsSchema = new mongoose.Schema({}, { discriminatorKey: 'globalType', timestamps: false }) .plugin(localizationPlugin, config.localization) .plugin(autopopulate) - .plugin(mongooseHidden); + .plugin(mongooseHidden()); const Globals = mongoose.model('globals', globalsSchema); Object.values(config.globals).forEach((globalConfig) => { const globalSchema = buildSchema(globalConfig.fields); + + globalSchema + .plugin(localizationPlugin, config.localization) + .plugin(autopopulate) + .plugin(mongooseHidden()); + Globals.discriminator(globalConfig.slug, globalSchema); }); From 9b2935a88fd4fb12eb119ac5f92b5e343ad65901 Mon Sep 17 00:00:00 2001 From: James Date: Mon, 6 Jul 2020 23:25:45 -0400 Subject: [PATCH 015/125] simplifies misc. globals functionality --- src/client/components/views/Global/Default.js | 14 +++++--------- src/globals/buildModel.js | 8 ++++---- src/globals/operations/findOne.js | 1 - 3 files changed, 9 insertions(+), 14 deletions(-) diff --git a/src/client/components/views/Global/Default.js b/src/client/components/views/Global/Default.js index ff61fa946..50da8b2b3 100644 --- a/src/client/components/views/Global/Default.js +++ b/src/client/components/views/Global/Default.js @@ -103,15 +103,11 @@ const DefaultGlobalView = (props) => { {data && (
    - {data && ( - <> - {data.updatedAt && ( -
  • -
    Last Modified
    -
    {format(new Date(data.updatedAt), 'MMMM do yyyy, h:mma')}
    -
  • - )} - + {data.updatedAt && ( +
  • +
    Last Modified
    +
    {format(new Date(data.updatedAt), 'MMMM do yyyy, h:mma')}
    +
  • )}
)} diff --git a/src/globals/buildModel.js b/src/globals/buildModel.js index ae6e69e30..3d7235216 100644 --- a/src/globals/buildModel.js +++ b/src/globals/buildModel.js @@ -1,15 +1,15 @@ const mongoose = require('mongoose'); const autopopulate = require('mongoose-autopopulate'); -const mongooseHidden = require('mongoose-hidden'); +const mongooseHidden = require('mongoose-hidden')(); const buildSchema = require('../mongoose/buildSchema'); const localizationPlugin = require('../localization/plugin'); const buildModel = (config) => { if (config.globals && config.globals.length > 0) { - const globalsSchema = new mongoose.Schema({}, { discriminatorKey: 'globalType', timestamps: false }) + const globalsSchema = new mongoose.Schema({}, { discriminatorKey: 'globalType', timestamps: true }) .plugin(localizationPlugin, config.localization) .plugin(autopopulate) - .plugin(mongooseHidden()); + .plugin(mongooseHidden); const Globals = mongoose.model('globals', globalsSchema); @@ -19,7 +19,7 @@ const buildModel = (config) => { globalSchema .plugin(localizationPlugin, config.localization) .plugin(autopopulate) - .plugin(mongooseHidden()); + .plugin(mongooseHidden); Globals.discriminator(globalConfig.slug, globalSchema); }); diff --git a/src/globals/operations/findOne.js b/src/globals/operations/findOne.js index e2f1630de..3f71d4dec 100644 --- a/src/globals/operations/findOne.js +++ b/src/globals/operations/findOne.js @@ -1,5 +1,4 @@ const executePolicy = require('../../auth/executePolicy'); -const { NotFound } = require('../../errors'); const performFieldOperations = require('../../fields/performFieldOperations'); const findOne = async (args) => { From 7879356bc5e4ae8bb2c96463ef713b53bb8faee9 Mon Sep 17 00:00:00 2001 From: James Date: Tue, 7 Jul 2020 11:26:04 -0400 Subject: [PATCH 016/125] misc responsive tweaks, improvements to usePayloadAPI isLoading state --- .../elements/FileDetails/index.scss | 1 + .../elements/PreviewButton/index.js | 4 +- .../forms/DraggableSection/index.scss | 1 + .../forms/field-types/Upload/index.scss | 5 +- .../views/collections/Edit/Default.js | 116 ++++++++++-------- .../views/collections/Edit/index.js | 3 +- src/client/hooks/usePayloadAPI.js | 13 +- 7 files changed, 83 insertions(+), 60 deletions(-) diff --git a/src/client/components/elements/FileDetails/index.scss b/src/client/components/elements/FileDetails/index.scss index b8ac26fe5..773cfa7b7 100644 --- a/src/client/components/elements/FileDetails/index.scss +++ b/src/client/components/elements/FileDetails/index.scss @@ -91,6 +91,7 @@ &__main-detail { border-top: $style-stroke-width-m solid white; order: 3; + width: 100%; } } } diff --git a/src/client/components/elements/PreviewButton/index.js b/src/client/components/elements/PreviewButton/index.js index 7174b2632..8c2c8f370 100644 --- a/src/client/components/elements/PreviewButton/index.js +++ b/src/client/components/elements/PreviewButton/index.js @@ -11,9 +11,9 @@ const PreviewButton = ({ generatePreviewURL }) => { const { getFields } = useForm(); const fields = getFields(); - const previewURL = (generatePreviewURL && typeof generatePreviewURL === 'function') ? generatePreviewURL(fields, token) : null; + if (generatePreviewURL && typeof generatePreviewURL === 'function') { + const previewURL = generatePreviewURL(fields, token); - if (previewURL) { return ( ); }; diff --git a/src/client/components/forms/field-types/File/index.js b/src/client/components/forms/field-types/File/index.js index 64ac56ed7..19565a33d 100644 --- a/src/client/components/forms/field-types/File/index.js +++ b/src/client/components/forms/field-types/File/index.js @@ -111,7 +111,7 @@ const File = (props) => { }; } - return null; + return () => { }; }, [handleDragIn, handleDragOut, handleDrop, dropRef]); useEffect(() => { diff --git a/src/client/components/forms/field-types/Relationship/index.js b/src/client/components/forms/field-types/Relationship/index.js index 0fedc1419..7ebbac380 100644 --- a/src/client/components/forms/field-types/Relationship/index.js +++ b/src/client/components/forms/field-types/Relationship/index.js @@ -336,13 +336,15 @@ const RelationshipFieldType = (props) => { useEffect(() => { const formatInitialData = (valueToFormat) => { if (hasMultipleRelations) { + const id = valueToFormat?.value?.id || valueToFormat?.value; + return { ...valueToFormat, - value: valueToFormat.value.id, + value: id, }; } - return valueToFormat.id; + return valueToFormat?.id || valueToFormat; }; if (dataToInitialize) { diff --git a/src/client/components/forms/field-types/Upload/index.js b/src/client/components/forms/field-types/Upload/index.js index 79dca1c3d..b395b0011 100644 --- a/src/client/components/forms/field-types/Upload/index.js +++ b/src/client/components/forms/field-types/Upload/index.js @@ -14,7 +14,7 @@ import SelectExistingModal from './SelectExisting'; import './index.scss'; -const { collections } = config; +const { collections, serverURL, routes: { api } } = config; const baseClass = 'upload'; @@ -43,10 +43,12 @@ const Upload = (props) => { const addModalSlug = `${path}-add`; const selectExistingModalSlug = `${path}-select-existing`; + const dataToInitialize = (typeof initialData === 'object' && initialData.id) ? initialData.id : initialData; + const fieldType = useFieldType({ path, required, - initialData: initialData?.id, + initialData: dataToInitialize, defaultValue, validate, }); @@ -66,10 +68,23 @@ const Upload = (props) => { ].filter(Boolean).join(' '); useEffect(() => { - if (initialData) { + if (typeof initialData === 'object' && initialData?.id) { setInternalValue(initialData); } - }, [initialData]); + + if (typeof initialData === 'string') { + const fetchFile = async () => { + const response = await fetch(`${serverURL}${api}/${relationTo}/${initialData}`); + + if (response.ok) { + const json = await response.json(); + setInternalValue(json); + } + }; + + fetchFile(); + } + }, [initialData, setInternalValue, relationTo]); return (
Date: Tue, 7 Jul 2020 14:44:37 -0400 Subject: [PATCH 018/125] modifies all auth request handlers to rely on express error handling, updates /me req handler to operation format --- src/auth/graphql/resolvers/me.js | 7 +++-- src/auth/operations/index.js | 2 ++ src/auth/operations/me.js | 29 +++++++++++++++++++ src/auth/requestHandlers/forgotPassword.js | 5 ++-- src/auth/requestHandlers/init.js | 6 ++-- src/auth/requestHandlers/login.js | 5 ++-- src/auth/requestHandlers/me.js | 27 ++++------------- src/auth/requestHandlers/policies.js | 5 ++-- src/auth/requestHandlers/refresh.js | 6 ++-- src/auth/requestHandlers/register.js | 5 ++-- src/auth/requestHandlers/registerFirstUser.js | 6 ++-- src/auth/requestHandlers/resetPassword.js | 6 ++-- src/auth/requestHandlers/update.js | 5 ++-- src/auth/routes.js | 2 +- src/collections/graphql/init.js | 6 +++- 15 files changed, 64 insertions(+), 58 deletions(-) create mode 100644 src/auth/operations/me.js diff --git a/src/auth/graphql/resolvers/me.js b/src/auth/graphql/resolvers/me.js index 729d73834..74a4380ec 100644 --- a/src/auth/graphql/resolvers/me.js +++ b/src/auth/graphql/resolvers/me.js @@ -1,6 +1,7 @@ -/* eslint-disable no-param-reassign */ -const meResolver = async (_, args, context) => { - return context.user; +const { me } = require('../../operations'); + +const meResolver = config => async (_, __, context) => { + return me({ req: context, config }); }; module.exports = meResolver; diff --git a/src/auth/operations/index.js b/src/auth/operations/index.js index 418e34150..7d7fafd05 100644 --- a/src/auth/operations/index.js +++ b/src/auth/operations/index.js @@ -7,6 +7,7 @@ const resetPassword = require('./resetPassword'); const registerFirstUser = require('./registerFirstUser'); const update = require('./update'); const policies = require('./policies'); +const me = require('./me'); module.exports = { login, @@ -18,4 +19,5 @@ module.exports = { resetPassword, registerFirstUser, policies, + me, }; diff --git a/src/auth/operations/me.js b/src/auth/operations/me.js new file mode 100644 index 000000000..7c816da11 --- /dev/null +++ b/src/auth/operations/me.js @@ -0,0 +1,29 @@ +const jwt = require('jsonwebtoken'); +const getExtractJWT = require('../getExtractJWT'); + +const me = async ({ req, config }) => { + try { + const extractJWT = getExtractJWT(config); + + if (req.user) { + const response = req.user; + + const token = extractJWT(req); + + if (token) { + const decoded = jwt.decode(token); + if (decoded) { + response.exp = decoded.exp; + } + } + + return response; + } + + return null; + } catch (error) { + throw error; + } +}; + +module.exports = me; diff --git a/src/auth/requestHandlers/forgotPassword.js b/src/auth/requestHandlers/forgotPassword.js index 96eda790f..742c7f614 100644 --- a/src/auth/requestHandlers/forgotPassword.js +++ b/src/auth/requestHandlers/forgotPassword.js @@ -1,8 +1,7 @@ const httpStatus = require('http-status'); -const formatErrorResponse = require('../../express/responses/formatError'); const { forgotPassword } = require('../operations'); -const forgotPasswordHandler = (config, email) => async (req, res) => { +const forgotPasswordHandler = (config, email) => async (req, res, next) => { try { await forgotPassword({ req, @@ -17,7 +16,7 @@ const forgotPasswordHandler = (config, email) => async (req, res) => { message: 'Success', }); } catch (error) { - return res.status(error.status || httpStatus.INTERNAL_SERVER_ERROR).json(formatErrorResponse(error)); + return next(error); } }; diff --git a/src/auth/requestHandlers/init.js b/src/auth/requestHandlers/init.js index 27e86d50a..a7891ffb0 100644 --- a/src/auth/requestHandlers/init.js +++ b/src/auth/requestHandlers/init.js @@ -1,13 +1,11 @@ -const httpStatus = require('http-status'); const { init } = require('../operations'); -const formatError = require('../../express/responses/formatError'); -const initHandler = async (req, res) => { +const initHandler = async (req, res, next) => { try { const initialized = await init({ Model: req.collection.Model }); return res.status(200).json({ initialized }); } catch (error) { - return res.status(error.status || httpStatus.INTERNAL_SERVER_ERROR).json(formatError(error)); + return next(error); } }; diff --git a/src/auth/requestHandlers/login.js b/src/auth/requestHandlers/login.js index 8d8bb8ba2..d72dfa305 100644 --- a/src/auth/requestHandlers/login.js +++ b/src/auth/requestHandlers/login.js @@ -1,8 +1,7 @@ const httpStatus = require('http-status'); -const formatErrorResponse = require('../../express/responses/formatError'); const { login } = require('../operations'); -const loginHandler = config => async (req, res) => { +const loginHandler = config => async (req, res, next) => { try { const token = await login({ req, @@ -18,7 +17,7 @@ const loginHandler = config => async (req, res) => { token, }); } catch (error) { - return res.status(error.status || httpStatus.INTERNAL_SERVER_ERROR).json(formatErrorResponse(error)); + return next(error); } }; diff --git a/src/auth/requestHandlers/me.js b/src/auth/requestHandlers/me.js index 223ac84da..b3847a31d 100644 --- a/src/auth/requestHandlers/me.js +++ b/src/auth/requestHandlers/me.js @@ -1,29 +1,12 @@ -const jwt = require('jsonwebtoken'); +const { me } = require('../operations'); -const meHandler = async (req, res, next) => { +const meHandler = config => async (req, res, next) => { try { - if (req.user) { - const response = req.user; - - if (req.headers.authorization && req.headers.authorization.indexOf('JWT') === 0) { - const token = req.headers.authorization.replace('JWT ', ''); - if (token) { - const decoded = jwt.decode(token); - - if (decoded.exp) { - response.exp = decoded.exp; - } - } - } - - return res.status(200).json(response); - } - return res.status(200).json(null); + const response = await me({ req, config }); + return res.status(200).json(response); } catch (err) { - next(err); + return next(err); } - - return next(); }; module.exports = meHandler; diff --git a/src/auth/requestHandlers/policies.js b/src/auth/requestHandlers/policies.js index 783f62fda..22dbdd38a 100644 --- a/src/auth/requestHandlers/policies.js +++ b/src/auth/requestHandlers/policies.js @@ -1,8 +1,7 @@ const httpStatus = require('http-status'); -const formatErrorResponse = require('../../express/responses/formatError'); const { policies } = require('../operations'); -const policiesHandler = config => async (req, res) => { +const policiesHandler = config => async (req, res, next) => { try { const policyResults = await policies({ req, @@ -12,7 +11,7 @@ const policiesHandler = config => async (req, res) => { return res.status(httpStatus.OK) .json(policyResults); } catch (error) { - return res.status(error.status || httpStatus.INTERNAL_SERVER_ERROR).json(formatErrorResponse(error)); + return next(error); } }; diff --git a/src/auth/requestHandlers/refresh.js b/src/auth/requestHandlers/refresh.js index 03291e8ec..ec3558b3d 100644 --- a/src/auth/requestHandlers/refresh.js +++ b/src/auth/requestHandlers/refresh.js @@ -1,9 +1,7 @@ -const httpStatus = require('http-status'); -const formatErrorResponse = require('../../express/responses/formatError'); const { refresh } = require('../operations'); const getExtractJWT = require('../getExtractJWT'); -const refreshHandler = config => async (req, res) => { +const refreshHandler = config => async (req, res, next) => { try { const extractJWT = getExtractJWT(config); const token = extractJWT(req); @@ -21,7 +19,7 @@ const refreshHandler = config => async (req, res) => { ...result, }); } catch (error) { - return res.status(error.status || httpStatus.INTERNAL_SERVER_ERROR).json(formatErrorResponse(error)); + return next(error); } }; diff --git a/src/auth/requestHandlers/register.js b/src/auth/requestHandlers/register.js index 03342c2be..bf7e15fa1 100644 --- a/src/auth/requestHandlers/register.js +++ b/src/auth/requestHandlers/register.js @@ -1,9 +1,8 @@ const httpStatus = require('http-status'); -const formatErrorResponse = require('../../express/responses/formatError'); const formatSuccessResponse = require('../../express/responses/formatSuccess'); const { register } = require('../operations'); -const registerHandler = config => async (req, res) => { +const registerHandler = config => async (req, res, next) => { try { const user = await register({ config, @@ -17,7 +16,7 @@ const registerHandler = config => async (req, res) => { doc: user, }); } catch (error) { - return res.status(error.status || httpStatus.UNAUTHORIZED).json(formatErrorResponse(error)); + return next(error); } }; diff --git a/src/auth/requestHandlers/registerFirstUser.js b/src/auth/requestHandlers/registerFirstUser.js index a53c21453..7b89179d7 100644 --- a/src/auth/requestHandlers/registerFirstUser.js +++ b/src/auth/requestHandlers/registerFirstUser.js @@ -1,8 +1,6 @@ -const httpStatus = require('http-status'); -const formatErrorResponse = require('../../express/responses/formatError'); const { registerFirstUser } = require('../operations'); -const registerFirstUserHandler = config => async (req, res) => { +const registerFirstUserHandler = config => async (req, res, next) => { try { const firstUser = await registerFirstUser({ req, @@ -13,7 +11,7 @@ const registerFirstUserHandler = config => async (req, res) => { return res.status(201).json(firstUser); } catch (error) { - return res.status(error.status || httpStatus.INTERNAL_SERVER_ERROR).json(formatErrorResponse(error)); + return next(error); } }; diff --git a/src/auth/requestHandlers/resetPassword.js b/src/auth/requestHandlers/resetPassword.js index 5406f3a33..73665abca 100644 --- a/src/auth/requestHandlers/resetPassword.js +++ b/src/auth/requestHandlers/resetPassword.js @@ -1,8 +1,7 @@ const httpStatus = require('http-status'); -const formatErrorResponse = require('../../express/responses/formatError'); const { resetPassword } = require('../operations'); -const resetPasswordHandler = config => async (req, res) => { +const resetPasswordHandler = config => async (req, res, next) => { try { const token = await resetPassword({ req, @@ -17,8 +16,7 @@ const resetPasswordHandler = config => async (req, res) => { token, }); } catch (error) { - return res.status(error.status || httpStatus.INTERNAL_SERVER_ERROR) - .json(formatErrorResponse(error)); + return next(error); } }; diff --git a/src/auth/requestHandlers/update.js b/src/auth/requestHandlers/update.js index 2447ee0e0..2bdd1e702 100644 --- a/src/auth/requestHandlers/update.js +++ b/src/auth/requestHandlers/update.js @@ -1,9 +1,8 @@ const httpStatus = require('http-status'); -const formatErrorResponse = require('../../express/responses/formatError'); const formatSuccessResponse = require('../../express/responses/formatSuccess'); const { update } = require('../operations'); -const updateHandler = async (req, res) => { +const updateHandler = async (req, res, next) => { try { const user = await update({ req, @@ -18,7 +17,7 @@ const updateHandler = async (req, res) => { doc: user, }); } catch (error) { - return res.status(httpStatus.UNAUTHORIZED).json(formatErrorResponse(error)); + return next(error); } }; diff --git a/src/auth/routes.js b/src/auth/routes.js index e051a8f25..fc326968a 100644 --- a/src/auth/routes.js +++ b/src/auth/routes.js @@ -46,7 +46,7 @@ const authRoutes = (collection, config, sendEmail) => { router .route(`/${slug}/me`) - .get(me); + .get(me(config)); router .route(`/${slug}/first-register`) diff --git a/src/collections/graphql/init.js b/src/collections/graphql/init.js index a77f9be51..1bf6872ec 100644 --- a/src/collections/graphql/init.js +++ b/src/collections/graphql/init.js @@ -141,12 +141,16 @@ function registerCollections() { type: 'text', required: true, }, + { + name: 'exp', + type: 'number', + }, ]), ); this.Query.fields[`me${singularLabel}`] = { type: collection.graphQL.jwt, - resolve: me, + resolve: me(this.config), }; this.Query.fields[`initialized${singularLabel}`] = { From 9391b51eb5614ef875b82789b1edf9775b5aff23 Mon Sep 17 00:00:00 2001 From: James Date: Tue, 7 Jul 2020 16:04:13 -0400 Subject: [PATCH 019/125] WIP - adds field-level policies to GraphQL --- .../BlockSelector/BlockSelection/index.js | 1 - src/graphql/schema/buildPoliciesType.js | 76 +++++++++++++++++-- 2 files changed, 71 insertions(+), 6 deletions(-) diff --git a/src/client/components/forms/field-types/Flexible/BlockSelector/BlockSelection/index.js b/src/client/components/forms/field-types/Flexible/BlockSelector/BlockSelection/index.js index b077ec2d0..f3236a40f 100644 --- a/src/client/components/forms/field-types/Flexible/BlockSelector/BlockSelection/index.js +++ b/src/client/components/forms/field-types/Flexible/BlockSelector/BlockSelection/index.js @@ -17,7 +17,6 @@ const BlockSelection = (props) => { } = block; const handleBlockSelection = () => { - console.log('adding'); close(); addRow(addRowIndex, slug); }; diff --git a/src/graphql/schema/buildPoliciesType.js b/src/graphql/schema/buildPoliciesType.js index 128e61c4d..fab56d244 100644 --- a/src/graphql/schema/buildPoliciesType.js +++ b/src/graphql/schema/buildPoliciesType.js @@ -2,15 +2,81 @@ const { GraphQLJSONObject } = require('graphql-type-json'); const { GraphQLBoolean, GraphQLNonNull, GraphQLObjectType } = require('graphql'); const formatName = require('../utilities/formatName'); -const buildFields = (label, operations) => { - const fields = {}; +const buildFields = (label, fieldsToBuild) => { + return fieldsToBuild.reduce((builtFields, field) => { + if (field.name) { + const fieldName = formatName(field.name); + + if (field.fields) { + return { + ...builtFields, + [field.name]: { + type: new GraphQLObjectType({ + name: `${label}${fieldName}Fields`, + fields: buildFields(`${label}${fieldName}`, field.fields), + }), + }, + }; + } + + return { + ...builtFields, + [field.name]: { + type: new GraphQLObjectType({ + name: `${label}${fieldName}Policies`, + fields: ['create', 'read', 'update', 'delete'].reduce((operations, operation) => { + const capitalizedOperation = operation.charAt(0).toUpperCase() + operation.slice(1); + + return { + ...operations, + [operation]: { + type: new GraphQLObjectType({ + name: `${label}${fieldName}${capitalizedOperation}Policy`, + fields: { + permission: { + type: new GraphQLNonNull(GraphQLBoolean), + }, + }, + }), + }, + }; + }, {}), + }), + }, + }; + } + + if (!field.name && field.fields) { + const subFields = buildFields(label, field.fields); + + return { + ...builtFields, + ...subFields, + }; + } + + return builtFields; + }, {}); +}; + +const buildEntity = (label, entityFields, operations) => { + const formattedLabel = formatName(label); + + const fields = { + fields: { + type: new GraphQLObjectType({ + name: formatName(`${formattedLabel}Fields`), + fields: buildFields(`${formattedLabel}Fields`, entityFields), + }), + }, + }; operations.forEach((operation) => { const capitalizedOperation = operation.charAt(0).toUpperCase() + operation.slice(1); fields[operation] = { type: new GraphQLObjectType({ - name: formatName(`${label}${capitalizedOperation}Policy`), + name: `${formattedLabel}${capitalizedOperation}Policy`, fields: { permission: { type: new GraphQLNonNull(GraphQLBoolean) }, where: { type: GraphQLJSONObject }, @@ -33,7 +99,7 @@ function buildPoliciesType() { fields[formatName(collection.slug)] = { type: new GraphQLObjectType({ name: formatName(`${collection.labels.singular}Policy`), - fields: buildFields(collection.labels.singular, ['create', 'read', 'update', 'delete']), + fields: buildEntity(collection.labels.singular, collection.fields, ['create', 'read', 'update', 'delete']), }), }; }); @@ -42,7 +108,7 @@ function buildPoliciesType() { fields[formatName(global.slug)] = { type: new GraphQLObjectType({ name: formatName(`${global.label}Policy`), - fields: buildFields(global.label, ['read', 'update']), + fields: buildEntity(global.label, global.fields, ['read', 'update']), }), }; }); From cdb81824079bafdd3a96f89d0d5091847897c4d0 Mon Sep 17 00:00:00 2001 From: James Date: Tue, 7 Jul 2020 18:46:14 -0400 Subject: [PATCH 020/125] WIP - progress to Dashboard and Card components --- demo/payload.config.js | 6 ++- src/client/components/elements/Card/index.js | 49 +++++++++++++++++ .../components/elements/Card/index.scss | 41 +++++++++++++++ src/client/components/elements/Nav/index.js | 42 ++++++++------- .../components/views/Dashboard/index.js | 52 ++++++++++++++++--- .../components/views/Dashboard/index.scss | 32 +++++++++++- src/client/scss/app.scss | 3 +- src/client/scss/type.scss | 10 +++- 8 files changed, 203 insertions(+), 32 deletions(-) create mode 100644 src/client/components/elements/Card/index.js create mode 100644 src/client/components/elements/Card/index.scss diff --git a/demo/payload.config.js b/demo/payload.config.js index 72bf76ec3..30afda498 100644 --- a/demo/payload.config.js +++ b/demo/payload.config.js @@ -51,7 +51,11 @@ module.exports = { StrictPolicies, Validations, ], - globals: [NavigationRepeater, GlobalWithPolicies, FlexibleGlobal], + globals: [ + NavigationRepeater, + GlobalWithPolicies, + FlexibleGlobal, + ], cookiePrefix: 'payload', serverURL: 'http://localhost:3000', cors: ['http://localhost', 'http://localhost:8080', 'http://localhost:8081'], diff --git a/src/client/components/elements/Card/index.js b/src/client/components/elements/Card/index.js new file mode 100644 index 000000000..7beda6261 --- /dev/null +++ b/src/client/components/elements/Card/index.js @@ -0,0 +1,49 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import Button from '../Button'; + +import './index.scss'; + +const baseClass = 'card'; + +const Card = (props) => { + const { title, actions, onClick } = props; + + const classes = [ + baseClass, + onClick && `${baseClass}--has-onclick`, + ].filter(Boolean).join(' '); + + return ( +
+
+ {title} +
+ {actions && ( +
+ {actions} +
+ )} + {onClick && ( +
+ ); +}; + +Card.defaultProps = { + actions: null, + onClick: undefined, +}; + +Card.propTypes = { + title: PropTypes.string.isRequired, + actions: PropTypes.node, + onClick: PropTypes.func, +}; + +export default Card; diff --git a/src/client/components/elements/Card/index.scss b/src/client/components/elements/Card/index.scss new file mode 100644 index 000000000..d0b6ed718 --- /dev/null +++ b/src/client/components/elements/Card/index.scss @@ -0,0 +1,41 @@ +@import '../../../scss/styles'; + +.card { + background: $color-background-gray; + padding: base(1.25) $baseline; + position: relative; + + h5 { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + &__actions { + position: relative; + z-index: 2; + margin-top: base(.5); + display: inline-flex; + + .btn { + margin: 0; + } + } + + &--has-onclick { + cursor: pointer; + + &:hover { + background: darken($color-background-gray, 3%); + } + } + + &__click { + position: absolute; + z-index: 1; + top: 0; + left: 0; + width: 100%; + height: 100%; + } +} diff --git a/src/client/components/elements/Nav/index.js b/src/client/components/elements/Nav/index.js index ca21f1bd9..6bb43a909 100644 --- a/src/client/components/elements/Nav/index.js +++ b/src/client/components/elements/Nav/index.js @@ -76,27 +76,31 @@ const Nav = () => { return null; })} - Globals - + + )}
{ const { setStepNav } = useStepNav(); + const { push } = useHistory(); useEffect(() => { setStepNav([]); @@ -24,12 +29,45 @@ const Dashboard = () => { return (
-

Dashboard

- Login -
- Pages List -
- Edit Page +
+

Collections

+
    + {collections.map((collection) => { + return ( +
  • + push({ pathname: `${admin}/collections/${collection.slug}` })} + actions={( +
  • + ); + })} +
+ {(globals && globals.length > 0) && ( + <> +

Globals

+
    + {globals.map((global) => { + return ( +
  • + +
  • + ); + })} +
+ + )} +
); }; diff --git a/src/client/components/views/Dashboard/index.scss b/src/client/components/views/Dashboard/index.scss index 7180ac6ab..ae982f4ba 100644 --- a/src/client/components/views/Dashboard/index.scss +++ b/src/client/components/views/Dashboard/index.scss @@ -1,3 +1,33 @@ -.dashboard { +@import '../../../scss/styles'; +.dashboard { + width: 100%; + padding-right: base(2); + + &__wrap { + padding: base(2); + background: white; + } + + &__label { + @extend %body; + color: $color-gray; + } + + &__card-list { + width: calc(100% + #{$baseline}); + margin: 0 - base(.5); + padding: 0; + list-style: none; + display: flex; + flex-wrap: wrap; + + li { + width: 25%; + } + + .card { + margin: 0 base(.5) $baseline; + } + } } diff --git a/src/client/scss/app.scss b/src/client/scss/app.scss index 14c64c67b..f8e8932e4 100644 --- a/src/client/scss/app.scss +++ b/src/client/scss/app.scss @@ -18,8 +18,7 @@ } html { - font-size: $baseline-body-size; - line-height: $baseline-px; + @extend %body; background: $color-background-gray; -webkit-font-smoothing: antialiased; overflow-y: scroll; diff --git a/src/client/scss/type.scss b/src/client/scss/type.scss index 6b33e54b2..832d74f95 100644 --- a/src/client/scss/type.scss +++ b/src/client/scss/type.scss @@ -63,9 +63,9 @@ %h5 { margin: 0; - font-size: base(.5); + font-size: base(.5625); line-height: 1.5; - font-weight: 700; + font-weight: 600; } %small { @@ -94,6 +94,12 @@ } } +%body { + font-size: $baseline-body-size; + line-height: $baseline-px; + font-weight: normal; +} + %code { font-size: base(.4); color: $color-gray; From c69d6c53a36f1b811e5e960de2a7f5d5a5429281 Mon Sep 17 00:00:00 2001 From: James Date: Tue, 7 Jul 2020 19:04:54 -0400 Subject: [PATCH 021/125] responsive Dashboard and Card components --- demo/collections/AllFields.js | 18 +++-- .../components/views/Dashboard/Default.js | 78 +++++++++++++++++++ .../components/views/Dashboard/index.js | 66 ++-------------- .../components/views/Dashboard/index.scss | 35 ++++++++- 4 files changed, 130 insertions(+), 67 deletions(-) create mode 100644 src/client/components/views/Dashboard/Default.js diff --git a/demo/collections/AllFields.js b/demo/collections/AllFields.js index 005c60274..89a99c735 100644 --- a/demo/collections/AllFields.js +++ b/demo/collections/AllFields.js @@ -105,12 +105,6 @@ const AllFields = { defaultValue: 'option-2', required: true, }, - { - name: 'checkbox', - type: 'checkbox', - label: 'Checkbox', - position: 'sidebar', - }, { type: 'row', fields: [ @@ -218,6 +212,18 @@ const AllFields = { label: 'Textarea', name: 'textarea', }, + { + name: 'slug', + type: 'text', + label: 'Slug', + position: 'sidebar', + }, + { + name: 'checkbox', + type: 'checkbox', + label: 'Checkbox', + position: 'sidebar', + }, ], timestamps: true, }; diff --git a/src/client/components/views/Dashboard/Default.js b/src/client/components/views/Dashboard/Default.js new file mode 100644 index 000000000..5d070b42f --- /dev/null +++ b/src/client/components/views/Dashboard/Default.js @@ -0,0 +1,78 @@ +import React, { useEffect } from 'react'; +import { useHistory } from 'react-router-dom'; +import config from 'payload/config'; +import { useStepNav } from '../../elements/StepNav'; +import Eyebrow from '../../elements/Eyebrow'; +import Card from '../../elements/Card'; +import Button from '../../elements/Button'; + +import './index.scss'; + +const { + collections, + globals, + routes: { + admin, + }, +} = config; + +const baseClass = 'dashboard'; + +const Dashboard = () => { + const { setStepNav } = useStepNav(); + const { push } = useHistory(); + + useEffect(() => { + setStepNav([]); + }, [setStepNav]); + + return ( +
+ +
+

Collections

+
    + {collections.map((collection) => { + return ( +
  • + push({ pathname: `${admin}/collections/${collection.slug}` })} + actions={( +
  • + ); + })} +
+ {(globals && globals.length > 0) && ( + <> +

Globals

+
    + {globals.map((global) => { + return ( +
  • + push({ pathname: `${admin}/globals/${global.slug}` })} + /> +
  • + ); + })} +
+ + )} +
+
+ ); +}; + +export default Dashboard; diff --git a/src/client/components/views/Dashboard/index.js b/src/client/components/views/Dashboard/index.js index ef7b98308..7d4cebd52 100644 --- a/src/client/components/views/Dashboard/index.js +++ b/src/client/components/views/Dashboard/index.js @@ -1,74 +1,20 @@ import React, { useEffect } from 'react'; -import { useHistory } from 'react-router-dom'; -import config from 'payload/config'; import { useStepNav } from '../../elements/StepNav'; -import Eyebrow from '../../elements/Eyebrow'; -import Card from '../../elements/Card'; -import Button from '../../elements/Button'; - -import './index.scss'; - -const { - collections, - globals, - routes: { - admin, - }, -} = config; - -const baseClass = 'dashboard'; +import RenderCustomComponent from '../../utilities/RenderCustomComponent'; +import DefaultDashboard from './Default'; const Dashboard = () => { const { setStepNav } = useStepNav(); - const { push } = useHistory(); useEffect(() => { setStepNav([]); }, [setStepNav]); return ( -
- -
-

Collections

-
    - {collections.map((collection) => { - return ( -
  • - push({ pathname: `${admin}/collections/${collection.slug}` })} - actions={( -
  • - ); - })} -
- {(globals && globals.length > 0) && ( - <> -

Globals

-
    - {globals.map((global) => { - return ( -
  • - -
  • - ); - })} -
- - )} -
-
+ ); }; diff --git a/src/client/components/views/Dashboard/index.scss b/src/client/components/views/Dashboard/index.scss index ae982f4ba..a461729d4 100644 --- a/src/client/components/views/Dashboard/index.scss +++ b/src/client/components/views/Dashboard/index.scss @@ -23,11 +23,44 @@ flex-wrap: wrap; li { - width: 25%; + width: 20%; } .card { margin: 0 base(.5) $baseline; } } + + @include large-break { + li { + width: 25%; + } + } + + @include mid-break { + padding-right: 0; + + &__wrap { + padding: $baseline; + } + + li { + width: 33.33%; + } + } + + @include small-break { + &__card-list { + width: calc(100% + #{base(.5)}); + margin: 0 - base(.25); + } + + li { + width: 50%; + } + + .card { + margin: 0 base(.25) base(.5); + } + } } From 8fc612dc35e265de164f1194dbb7ac4a8e61f03a Mon Sep 17 00:00:00 2001 From: James Date: Tue, 7 Jul 2020 19:23:43 -0400 Subject: [PATCH 022/125] improves naming conventions of Policies GraphQL types --- src/graphql/schema/buildPoliciesType.js | 45 ++++++++++++------------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/src/graphql/schema/buildPoliciesType.js b/src/graphql/schema/buildPoliciesType.js index fab56d244..90a90ed00 100644 --- a/src/graphql/schema/buildPoliciesType.js +++ b/src/graphql/schema/buildPoliciesType.js @@ -7,40 +7,39 @@ const buildFields = (label, fieldsToBuild) => { if (field.name) { const fieldName = formatName(field.name); - if (field.fields) { + const objectTypeFields = ['create', 'read', 'update', 'delete'].reduce((operations, operation) => { + const capitalizedOperation = operation.charAt(0).toUpperCase() + operation.slice(1); + return { - ...builtFields, - [field.name]: { + ...operations, + [operation]: { type: new GraphQLObjectType({ - name: `${label}${fieldName}Fields`, - fields: buildFields(`${label}${fieldName}`, field.fields), + name: `${label}_${fieldName}_${capitalizedOperation}`, + fields: { + permission: { + type: new GraphQLNonNull(GraphQLBoolean), + }, + }, }), }, }; + }, {}); + + if (field.fields) { + objectTypeFields.fields = { + type: new GraphQLObjectType({ + name: `${label}_${fieldName}_Fields`, + fields: buildFields(`${label}_${fieldName}`, field.fields), + }), + }; } return { ...builtFields, [field.name]: { type: new GraphQLObjectType({ - name: `${label}${fieldName}Policies`, - fields: ['create', 'read', 'update', 'delete'].reduce((operations, operation) => { - const capitalizedOperation = operation.charAt(0).toUpperCase() + operation.slice(1); - - return { - ...operations, - [operation]: { - type: new GraphQLObjectType({ - name: `${label}${fieldName}${capitalizedOperation}Policy`, - fields: { - permission: { - type: new GraphQLNonNull(GraphQLBoolean), - }, - }, - }), - }, - }; - }, {}), + name: `${label}_${fieldName}`, + fields: objectTypeFields, }), }, }; From 76499696b35243e88a481db84394b2974dd392dd Mon Sep 17 00:00:00 2001 From: James Date: Tue, 7 Jul 2020 19:48:43 -0400 Subject: [PATCH 023/125] implements default sorting based on timestamps or ID --- demo/collections/AllFields.js | 13 +++++-------- demo/collections/FlexibleContent.js | 1 - .../components/views/collections/Edit/index.scss | 4 ++++ src/collections/operations/find.js | 14 +++++++++++--- 4 files changed, 20 insertions(+), 12 deletions(-) diff --git a/demo/collections/AllFields.js b/demo/collections/AllFields.js index 89a99c735..b2420a70f 100644 --- a/demo/collections/AllFields.js +++ b/demo/collections/AllFields.js @@ -30,14 +30,8 @@ const AllFields = { defaultValue: 'Default Value', unique: true, policies: { - create: () => { - console.log('trying to set text'); - return false; - }, - update: ({ req: { user } }) => { - const result = checkRole(['admin'], user); - return result; - }, + create: ({ req: { user } }) => checkRole(['admin'], user), + update: ({ req: { user } }) => checkRole(['admin'], user), read: ({ req: { user } }) => Boolean(user), }, }, @@ -217,6 +211,9 @@ const AllFields = { type: 'text', label: 'Slug', position: 'sidebar', + localized: true, + unique: true, + required: true, }, { name: 'checkbox', diff --git a/demo/collections/FlexibleContent.js b/demo/collections/FlexibleContent.js index 878c3dea6..543d7fa73 100644 --- a/demo/collections/FlexibleContent.js +++ b/demo/collections/FlexibleContent.js @@ -23,5 +23,4 @@ module.exports = { required: true, }, ], - timestamps: true, }; diff --git a/src/client/components/views/collections/Edit/index.scss b/src/client/components/views/collections/Edit/index.scss index daac97294..81e44d531 100644 --- a/src/client/components/views/collections/Edit/index.scss +++ b/src/client/components/views/collections/Edit/index.scss @@ -96,6 +96,10 @@ } } + &__sidebar-fields { + padding-right: $baseline; + } + &__meta { margin: 0; padding-top: $baseline; diff --git a/src/collections/operations/find.js b/src/collections/operations/find.js index 47bfde193..c4950f86f 100644 --- a/src/collections/operations/find.js +++ b/src/collections/operations/find.js @@ -1,5 +1,3 @@ -const deepmerge = require('deepmerge'); -const combineMerge = require('../../utilities/combineMerge'); const executePolicy = require('../../auth/executePolicy'); const performFieldOperations = require('../../fields/performFieldOperations'); @@ -55,7 +53,6 @@ const find = async (args) => { query, page, limit, - sort, depth, Model, req: { @@ -63,8 +60,19 @@ const find = async (args) => { fallbackLocale, payloadAPI, }, + config, } = options; + let { sort } = options; + + if (!sort) { + if (config.timestamps) { + sort = '-createdAt'; + } else { + sort = '-_id'; + } + } + const optionsToExecute = { page: page || 1, limit: limit || 10, From ac5caf4da2274fbdb08c5f388fe8eccd3dfc8f8a Mon Sep 17 00:00:00 2001 From: James Date: Tue, 7 Jul 2020 21:41:34 -0400 Subject: [PATCH 024/125] further separates contexts within Form for performance reasons --- .../elements/DuplicateDocument/index.js | 2 +- .../elements/PreviewButton/index.js | 2 +- .../components/forms/Form/FieldContext.js | 3 - .../components/forms/Form/FormContext.js | 3 - src/client/components/forms/Form/context.js | 26 ++++++ src/client/components/forms/Form/index.js | 87 ++++++++----------- .../components/forms/Form/initContextState.js | 3 - src/client/components/forms/Form/useForm.js | 4 - .../components/forms/Form/useFormFields.js | 4 - src/client/components/forms/Submit/index.js | 9 +- .../forms/field-types/Auth/APIKey.js | 18 ++-- .../forms/field-types/Auth/index.js | 8 +- .../field-types/ConfirmPassword/index.js | 2 +- .../forms/field-types/Flexible/index.js | 9 +- .../forms/field-types/Number/index.js | 12 ++- .../forms/field-types/Repeater/index.js | 49 +++++------ .../components/forms/useFieldType/index.js | 11 ++- .../components/forms/withCondition/index.js | 2 +- .../modals/LeaveWithoutSaving/index.js | 42 +++++---- src/client/hooks/useTitle.js | 2 +- src/fields/validations.js | 2 +- 21 files changed, 156 insertions(+), 144 deletions(-) delete mode 100644 src/client/components/forms/Form/FieldContext.js delete mode 100644 src/client/components/forms/Form/FormContext.js create mode 100644 src/client/components/forms/Form/context.js delete mode 100644 src/client/components/forms/Form/useForm.js delete mode 100644 src/client/components/forms/Form/useFormFields.js diff --git a/src/client/components/elements/DuplicateDocument/index.js b/src/client/components/elements/DuplicateDocument/index.js index 40c27aafb..6a1ed11b6 100644 --- a/src/client/components/elements/DuplicateDocument/index.js +++ b/src/client/components/elements/DuplicateDocument/index.js @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import { useHistory } from 'react-router-dom'; import config from 'payload/config'; import Button from '../Button'; -import useForm from '../../forms/Form/useForm'; +import { useForm } from '../../forms/Form/context'; import './index.scss'; diff --git a/src/client/components/elements/PreviewButton/index.js b/src/client/components/elements/PreviewButton/index.js index 8c2c8f370..3a444dd82 100644 --- a/src/client/components/elements/PreviewButton/index.js +++ b/src/client/components/elements/PreviewButton/index.js @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import useForm from '../../forms/Form/useForm'; +import { useForm } from '../../forms/Form/context'; import { useUser } from '../../data/User'; import Button from '../Button'; diff --git a/src/client/components/forms/Form/FieldContext.js b/src/client/components/forms/Form/FieldContext.js deleted file mode 100644 index 7c0127fbd..000000000 --- a/src/client/components/forms/Form/FieldContext.js +++ /dev/null @@ -1,3 +0,0 @@ -import { createContext } from 'react'; - -export default createContext({}); diff --git a/src/client/components/forms/Form/FormContext.js b/src/client/components/forms/Form/FormContext.js deleted file mode 100644 index 7c0127fbd..000000000 --- a/src/client/components/forms/Form/FormContext.js +++ /dev/null @@ -1,3 +0,0 @@ -import { createContext } from 'react'; - -export default createContext({}); diff --git a/src/client/components/forms/Form/context.js b/src/client/components/forms/Form/context.js new file mode 100644 index 000000000..efacd8df4 --- /dev/null +++ b/src/client/components/forms/Form/context.js @@ -0,0 +1,26 @@ +import { createContext, useContext } from 'react'; + +const FormContext = createContext({}); +const FieldContext = createContext({}); +const SubmittedContext = createContext(false); +const ProcessingContext = createContext(false); +const ModifiedContext = createContext(false); + +const useForm = () => useContext(FormContext); +const useFormFields = () => useContext(FieldContext); +const useFormSubmitted = () => useContext(SubmittedContext); +const useFormProcessing = () => useContext(ProcessingContext); +const useFormModified = () => useContext(ModifiedContext); + +export { + FormContext, + FieldContext, + SubmittedContext, + ProcessingContext, + ModifiedContext, + useForm, + useFormFields, + useFormSubmitted, + useFormProcessing, + useFormModified, +}; diff --git a/src/client/components/forms/Form/index.js b/src/client/components/forms/Form/index.js index 36846885a..a4cba6417 100644 --- a/src/client/components/forms/Form/index.js +++ b/src/client/components/forms/Form/index.js @@ -1,13 +1,11 @@ import React, { - useReducer, useEffect, useRef, + useReducer, useEffect, useRef, useState, } from 'react'; import { objectToFormData } from 'object-to-formdata'; import { useHistory } from 'react-router-dom'; import PropTypes from 'prop-types'; import { unflatten } from 'flatley'; import HiddenInput from '../field-types/HiddenInput'; -import FormContext from './FormContext'; -import FieldContext from './FieldContext'; import { useLocale } from '../../utilities/Locale'; import { useStatusList } from '../../elements/Status'; import { requests } from '../../../api'; @@ -16,6 +14,8 @@ import { useUser } from '../../data/User'; import fieldReducer from './fieldReducer'; import initContextState from './initContextState'; +import { SubmittedContext, ProcessingContext, ModifiedContext, FormContext, FieldContext } from './context'; + import './index.scss'; const baseClass = 'form'; @@ -43,7 +43,7 @@ const Form = (props) => { ajax, method, action, - handleAjaxResponse, + handleResponse, onSuccess, children, className, @@ -56,6 +56,10 @@ const Form = (props) => { const { replaceStatus, addStatus, clearStatus } = useStatusList(); const { refreshCookie } = useUser(); + const [modified, setModified] = useState(false); + const [processing, setProcessing] = useState(false); + const [submitted, setSubmitted] = useState(false); + const contextRef = useRef({ ...initContextState }); const [fields, dispatchFields] = useReducer(fieldReducer, {}); @@ -69,7 +73,7 @@ const Form = (props) => { } e.stopPropagation(); - contextRef.current.setSubmitted(true); + setSubmitted(true); const isValid = contextRef.current.validateForm(); @@ -106,17 +110,17 @@ const Form = (props) => { }); const formData = contextRef.current.createFormData(); - contextRef.current.setProcessing(true); + setProcessing(true); // Make the API call from the action return requests[method.toLowerCase()](action, { body: formData, }).then((res) => { - contextRef.current.setModified(false); - if (typeof handleAjaxResponse === 'function') return handleAjaxResponse(res); + setModified(false); + if (typeof handleResponse === 'function') return handleResponse(res); return res.json().then((json) => { - contextRef.current.setProcessing(false); + setProcessing(false); clearStatus(); if (res.status < 400) { @@ -144,9 +148,7 @@ const Form = (props) => { } if (Array.isArray(json.errors)) { - const [fieldErrors, nonFieldErrors] = json.errors.reduce(([fieldErrs, nonFieldErrs], err) => { - return err.field && err.message ? [[...fieldErrs, err], nonFieldErrs] : [fieldErrs, [...nonFieldErrs, err]]; - }, [[], []]); + const [fieldErrors, nonFieldErrors] = json.errors.reduce(([fieldErrs, nonFieldErrs], err) => (err.field && err.message ? [[...fieldErrs, err], nonFieldErrs] : [fieldErrs, [...nonFieldErrs, err]]), [[], []]); fieldErrors.forEach((err) => { dispatchFields({ @@ -193,17 +195,11 @@ const Form = (props) => { return true; }; - contextRef.current.getFields = () => { - return contextRef.current.fields; - }; + contextRef.current.getFields = () => contextRef.current.fields; - contextRef.current.getField = (path) => { - return contextRef.current.fields[path]; - }; + contextRef.current.getField = (path) => contextRef.current.fields[path]; - contextRef.current.getData = () => { - return reduceFieldsToValues(contextRef.current.fields, true); - }; + contextRef.current.getData = () => reduceFieldsToValues(contextRef.current.fields, true); contextRef.current.getSiblingData = (path) => { let siblingFields = contextRef.current.fields; @@ -247,40 +243,26 @@ const Form = (props) => { return unflattenedData?.[name]; }; - contextRef.current.getUnflattenedValues = () => { - return reduceFieldsToValues(contextRef.current.fields); - }; + contextRef.current.getUnflattenedValues = () => reduceFieldsToValues(contextRef.current.fields); - contextRef.current.validateForm = () => { - return !Object.values(contextRef.current.fields).some((field) => { - return field.valid === false; - }); - }; + contextRef.current.validateForm = () => !Object.values(contextRef.current.fields).some((field) => field.valid === false); contextRef.current.createFormData = () => { const data = reduceFieldsToValues(contextRef.current.fields); return objectToFormData(data, { indices: true }); }; - contextRef.current.setModified = (modified) => { - contextRef.current.modified = modified; - }; - - contextRef.current.setSubmitted = (submitted) => { - contextRef.current.submitted = submitted; - }; - - contextRef.current.setProcessing = (processing) => { - contextRef.current.processing = processing; - }; + contextRef.current.setModified = setModified; + contextRef.current.setProcessing = setProcessing; + contextRef.current.setSubmitted = setSubmitted; useThrottledEffect(() => { refreshCookie(); }, 15000, [fields]); useEffect(() => { - contextRef.current.modified = false; - }, [locale, contextRef.current.modified]); + setModified(false); + }, [locale]); const classes = [ className, @@ -301,13 +283,20 @@ const Form = (props) => { ...contextRef.current, }} > - - {children} + + + + + {children} + + + + ); }; @@ -318,7 +307,7 @@ Form.defaultProps = { ajax: true, method: 'POST', action: '', - handleAjaxResponse: null, + handleResponse: null, onSuccess: null, className: '', disableSuccessStatus: false, @@ -331,7 +320,7 @@ Form.propTypes = { ajax: PropTypes.bool, method: PropTypes.oneOf(['post', 'POST', 'get', 'GET', 'put', 'PUT', 'delete', 'DELETE']), action: PropTypes.string, - handleAjaxResponse: PropTypes.func, + handleResponse: PropTypes.func, onSuccess: PropTypes.func, children: PropTypes.oneOfType([ PropTypes.arrayOf(PropTypes.node), diff --git a/src/client/components/forms/Form/initContextState.js b/src/client/components/forms/Form/initContextState.js index 9ae8d322d..ff449921b 100644 --- a/src/client/components/forms/Form/initContextState.js +++ b/src/client/components/forms/Form/initContextState.js @@ -1,7 +1,4 @@ export default { - processing: false, - modified: false, - submitted: false, getFields: () => { }, getField: () => { }, getData: () => { }, diff --git a/src/client/components/forms/Form/useForm.js b/src/client/components/forms/Form/useForm.js deleted file mode 100644 index 6d968ee4e..000000000 --- a/src/client/components/forms/Form/useForm.js +++ /dev/null @@ -1,4 +0,0 @@ -import { useContext } from 'react'; -import FormContext from './FormContext'; - -export default () => useContext(FormContext); diff --git a/src/client/components/forms/Form/useFormFields.js b/src/client/components/forms/Form/useFormFields.js deleted file mode 100644 index 749ef7627..000000000 --- a/src/client/components/forms/Form/useFormFields.js +++ /dev/null @@ -1,4 +0,0 @@ -import { useContext } from 'react'; -import FieldContext from './FieldContext'; - -export default () => useContext(FieldContext); diff --git a/src/client/components/forms/Submit/index.js b/src/client/components/forms/Submit/index.js index 85b11ce3b..7dc4833be 100644 --- a/src/client/components/forms/Submit/index.js +++ b/src/client/components/forms/Submit/index.js @@ -1,6 +1,6 @@ -import React, { useContext } from 'react'; +import React from 'react'; import PropTypes from 'prop-types'; -import FormContext from '../Form/FormContext'; +import { useFormProcessing } from '../Form/context'; import Button from '../../elements/Button'; import './index.scss'; @@ -8,12 +8,13 @@ import './index.scss'; const baseClass = 'form-submit'; const FormSubmit = ({ children }) => { - const formContext = useContext(FormContext); + const processing = useFormProcessing(); + return (
diff --git a/src/client/components/forms/field-types/Auth/APIKey.js b/src/client/components/forms/field-types/Auth/APIKey.js index 3b2461784..706740a9b 100644 --- a/src/client/components/forms/field-types/Auth/APIKey.js +++ b/src/client/components/forms/field-types/Auth/APIKey.js @@ -1,4 +1,4 @@ -import React, { useMemo } from 'react'; +import React, { useMemo, useState, useEffect } from 'react'; import PropTypes from 'prop-types'; import { v4 as uuidv4 } from 'uuid'; import useFieldType from '../../useFieldType'; @@ -6,19 +6,21 @@ import Label from '../../Label'; import Button from '../../../elements/Button'; import CopyToClipboard from '../../../elements/CopyToClipboard'; import { text } from '../../../../../fields/validations'; -import useFormFields from '../../Form/useFormFields'; +import { useFormFields } from '../../Form/context'; import './index.scss'; const path = 'apiKey'; const baseClass = 'api-key'; -const validate = val => text(val, { minLength: 24, maxLength: 48 }); +const validate = (val) => text(val, { minLength: 24, maxLength: 48 }); const APIKey = (props) => { const { initialData, } = props; + const [initialAPIKey, setInitialAPIKey] = useState(null); + const { getField } = useFormFields(); const apiKey = getField(path); @@ -36,7 +38,7 @@ const APIKey = (props) => { const fieldType = useFieldType({ path: 'apiKey', - initialData: initialData || uuidv4(), + initialData: initialData || initialAPIKey, validate, }); @@ -45,6 +47,10 @@ const APIKey = (props) => { setValue, } = fieldType; + useEffect(() => { + setInitialAPIKey(uuidv4()); + }, []); + const classes = [ 'field-type', 'api-key', @@ -52,7 +58,7 @@ const APIKey = (props) => { ].filter(Boolean).join(' '); return ( - <> +
@@ -121,7 +120,7 @@ ActionPanel.propTypes = { singularLabel: PropTypes.string, addRow: PropTypes.func.isRequired, removeRow: PropTypes.func.isRequired, - blockType: PropTypes.oneOf(['flexible', 'repeater']), + blockType: PropTypes.oneOf(['blocks', 'array']), verticalAlignment: PropTypes.oneOf(['top', 'center', 'sticky']), isHovered: PropTypes.bool, rowIndex: PropTypes.number.isRequired, diff --git a/src/client/components/forms/DraggableSection/PositionPanel/index.scss b/src/client/components/forms/DraggableSection/PositionPanel/index.scss index 61b92d3e1..6f210cdd9 100644 --- a/src/client/components/forms/DraggableSection/PositionPanel/index.scss +++ b/src/client/components/forms/DraggableSection/PositionPanel/index.scss @@ -61,7 +61,7 @@ $controls-top-adjustment: base(.1); // External scopes -.field-type.flexible { +.field-type.blocks { .position-panel { &__controls-container { min-height: calc(100% + #{$controls-top-adjustment}); diff --git a/src/client/components/forms/DraggableSection/index.js b/src/client/components/forms/DraggableSection/index.js index a5d33f5b7..06a46d67e 100644 --- a/src/client/components/forms/DraggableSection/index.js +++ b/src/client/components/forms/DraggableSection/index.js @@ -70,7 +70,7 @@ const DraggableSection = (props) => {
- {blockType === 'flexible' && ( + {blockType === 'blocks' && (
{ +const Array = (props) => { const { label, name, @@ -123,7 +123,7 @@ const Repeater = (props) => { message={errorMessage} /> - + {(provided) => (
{ { ); }; -Repeater.defaultProps = { +Array.defaultProps = { label: '', defaultValue: [], initialData: [], - validate: repeater, + validate: array, required: false, maxRows: undefined, minRows: undefined, @@ -185,7 +185,7 @@ Repeater.defaultProps = { permissions: {}, }; -Repeater.propTypes = { +Array.propTypes = { defaultValue: PropTypes.arrayOf( PropTypes.shape({}), ), @@ -211,4 +211,4 @@ Repeater.propTypes = { }), }; -export default withCondition(Repeater); +export default withCondition(Array); diff --git a/src/client/components/forms/field-types/Repeater/index.scss b/src/client/components/forms/field-types/Array/index.scss similarity index 91% rename from src/client/components/forms/field-types/Repeater/index.scss rename to src/client/components/forms/field-types/Array/index.scss index 1ea46f813..f5c4eceb5 100644 --- a/src/client/components/forms/field-types/Repeater/index.scss +++ b/src/client/components/forms/field-types/Array/index.scss @@ -1,6 +1,6 @@ @import '../../../../scss/styles.scss'; -.field-type.repeater { +.field-type.array { background: white; &__add-button-wrap { @@ -26,7 +26,7 @@ } } - .field-type.repeater { + .field-type.array { .field-type.repeater__add-button-wrap { margin-left: base(2.65); } diff --git a/src/client/components/forms/field-types/Flexible/BlockSelector/BlockSearch/index.js b/src/client/components/forms/field-types/Blocks/BlockSelector/BlockSearch/index.js similarity index 100% rename from src/client/components/forms/field-types/Flexible/BlockSelector/BlockSearch/index.js rename to src/client/components/forms/field-types/Blocks/BlockSelector/BlockSearch/index.js diff --git a/src/client/components/forms/field-types/Flexible/BlockSelector/BlockSearch/index.scss b/src/client/components/forms/field-types/Blocks/BlockSelector/BlockSearch/index.scss similarity index 100% rename from src/client/components/forms/field-types/Flexible/BlockSelector/BlockSearch/index.scss rename to src/client/components/forms/field-types/Blocks/BlockSelector/BlockSearch/index.scss diff --git a/src/client/components/forms/field-types/Flexible/BlockSelector/BlockSelection/index.js b/src/client/components/forms/field-types/Blocks/BlockSelector/BlockSelection/index.js similarity index 100% rename from src/client/components/forms/field-types/Flexible/BlockSelector/BlockSelection/index.js rename to src/client/components/forms/field-types/Blocks/BlockSelector/BlockSelection/index.js diff --git a/src/client/components/forms/field-types/Flexible/BlockSelector/BlockSelection/index.scss b/src/client/components/forms/field-types/Blocks/BlockSelector/BlockSelection/index.scss similarity index 100% rename from src/client/components/forms/field-types/Flexible/BlockSelector/BlockSelection/index.scss rename to src/client/components/forms/field-types/Blocks/BlockSelector/BlockSelection/index.scss diff --git a/src/client/components/forms/field-types/Flexible/BlockSelector/BlocksContainer/index.js b/src/client/components/forms/field-types/Blocks/BlockSelector/BlocksContainer/index.js similarity index 100% rename from src/client/components/forms/field-types/Flexible/BlockSelector/BlocksContainer/index.js rename to src/client/components/forms/field-types/Blocks/BlockSelector/BlocksContainer/index.js diff --git a/src/client/components/forms/field-types/Flexible/BlockSelector/BlocksContainer/index.scss b/src/client/components/forms/field-types/Blocks/BlockSelector/BlocksContainer/index.scss similarity index 100% rename from src/client/components/forms/field-types/Flexible/BlockSelector/BlocksContainer/index.scss rename to src/client/components/forms/field-types/Blocks/BlockSelector/BlocksContainer/index.scss diff --git a/src/client/components/forms/field-types/Flexible/BlockSelector/index.js b/src/client/components/forms/field-types/Blocks/BlockSelector/index.js similarity index 100% rename from src/client/components/forms/field-types/Flexible/BlockSelector/index.js rename to src/client/components/forms/field-types/Blocks/BlockSelector/index.js diff --git a/src/client/components/forms/field-types/Flexible/BlockSelector/index.scss b/src/client/components/forms/field-types/Blocks/BlockSelector/index.scss similarity index 100% rename from src/client/components/forms/field-types/Flexible/BlockSelector/index.scss rename to src/client/components/forms/field-types/Blocks/BlockSelector/index.scss diff --git a/src/client/components/forms/field-types/Flexible/index.js b/src/client/components/forms/field-types/Blocks/index.js similarity index 95% rename from src/client/components/forms/field-types/Flexible/index.js rename to src/client/components/forms/field-types/Blocks/index.js index 21d215c61..ab3cc80d9 100644 --- a/src/client/components/forms/field-types/Flexible/index.js +++ b/src/client/components/forms/field-types/Blocks/index.js @@ -15,13 +15,13 @@ import Error from '../../Error'; import useFieldType from '../../useFieldType'; import Popup from '../../../elements/Popup'; import BlockSelector from './BlockSelector'; -import { flexible } from '../../../../../fields/validations'; +import { blocks } from '../../../../../fields/validations'; import './index.scss'; -const baseClass = 'field-type flexible'; +const baseClass = 'field-type blocks'; -const Flexible = (props) => { +const Blocks = (props) => { const { label, name, @@ -138,7 +138,7 @@ const Flexible = (props) => { /> - + {(provided) => (
{ { ); }; -Flexible.defaultProps = { +Blocks.defaultProps = { label: '', defaultValue: [], initialData: [], singularLabel: 'Block', - validate: flexible, + validate: blocks, required: false, maxRows: undefined, minRows: undefined, permissions: {}, }; -Flexible.propTypes = { +Blocks.propTypes = { defaultValue: PropTypes.arrayOf( PropTypes.shape({}), ), @@ -258,4 +258,4 @@ Flexible.propTypes = { }), }; -export default withCondition(Flexible); +export default withCondition(Blocks); diff --git a/src/client/components/forms/field-types/Flexible/index.scss b/src/client/components/forms/field-types/Blocks/index.scss similarity index 93% rename from src/client/components/forms/field-types/Flexible/index.scss rename to src/client/components/forms/field-types/Blocks/index.scss index 8d5702e68..b1f94c9fa 100644 --- a/src/client/components/forms/field-types/Flexible/index.scss +++ b/src/client/components/forms/field-types/Blocks/index.scss @@ -1,6 +1,6 @@ @import '../../../../scss/styles.scss'; -.field-type.flexible { +.field-type.blocks { &__add-button-wrap { diff --git a/src/client/components/forms/field-types/Relationship/index.js b/src/client/components/forms/field-types/Relationship/index.js index 7ebbac380..aaae16cf3 100644 --- a/src/client/components/forms/field-types/Relationship/index.js +++ b/src/client/components/forms/field-types/Relationship/index.js @@ -62,7 +62,7 @@ class Relationship extends Component { if (relationsToSearch.length > 0) { some(relationsToSearch, async (relation, callback) => { - const collection = collections.find(coll => coll.slug === relation); + const collection = collections.find((coll) => coll.slug === relation); const fieldToSearch = collection.useAsTitle || 'id'; const searchParam = search ? `&where[${fieldToSearch}][like]=${search}` : ''; const response = await fetch(`${serverURL}${api}/${relation}?limit=${maxResultsPerRequest}&page=${lastLoadedPage}${searchParam}`); @@ -99,7 +99,7 @@ class Relationship extends Component { const { hasMany } = this.props; if (hasMany && Array.isArray(selectedValue)) { - return selectedValue.map(val => val.value); + return selectedValue.map((val) => val.value); } return selectedValue ? selectedValue.value : selectedValue; @@ -126,9 +126,9 @@ class Relationship extends Component { }); } else if (value) { if (hasMany) { - foundValue = value.map(val => options.find(option => option.value === val)); + foundValue = value.map((val) => options.find((option) => option.value === val)); } else { - foundValue = options.find(option => option.value === value); + foundValue = options.find((option) => option.value === value); } } @@ -138,13 +138,13 @@ class Relationship extends Component { addOptions = (data, relation) => { const { hasMultipleRelations } = this.props; const { lastLoadedPage, options } = this.state; - const collection = collections.find(coll => coll.slug === relation); + const collection = collections.find((coll) => coll.slug === relation); if (!hasMultipleRelations) { this.setState({ options: [ ...options, - ...data.docs.map(doc => ({ + ...data.docs.map((doc) => ({ label: doc[collection.useAsTitle || 'id'], value: doc.id, })), @@ -152,17 +152,15 @@ class Relationship extends Component { }); } else { const allOptionGroups = [...options]; - const optionsToAddTo = allOptionGroups.find(optionGroup => optionGroup.label === collection.labels.plural); + const optionsToAddTo = allOptionGroups.find((optionGroup) => optionGroup.label === collection.labels.plural); - const newOptions = data.docs.map((doc) => { - return { - label: doc[collection.useAsTitle || 'id'], - value: { - relationTo: collection.slug, - value: doc.id, - }, - }; - }); + const newOptions = data.docs.map((doc) => ({ + label: doc[collection.useAsTitle || 'id'], + value: { + relationTo: collection.slug, + value: doc.id, + }, + })); if (optionsToAddTo) { optionsToAddTo.options = [ @@ -281,13 +279,9 @@ Relationship.defaultProps = { Relationship.propTypes = { readOnly: PropTypes.bool, relationTo: PropTypes.oneOfType([ - PropTypes.oneOf(Object.keys(collections).map((key) => { - return collections[key].slug; - })), + PropTypes.oneOf(Object.keys(collections).map((key) => collections[key].slug)), PropTypes.arrayOf( - PropTypes.oneOf(Object.keys(collections).map((key) => { - return collections[key].slug; - })), + PropTypes.oneOf(Object.keys(collections).map((key) => collections[key].slug)), ), ]).isRequired, required: PropTypes.bool, diff --git a/src/client/components/forms/field-types/Select/index.js b/src/client/components/forms/field-types/Select/index.js index fba1d3a29..b0e5ec1d4 100644 --- a/src/client/components/forms/field-types/Select/index.js +++ b/src/client/components/forms/field-types/Select/index.js @@ -10,7 +10,7 @@ import { select } from '../../../../../fields/validations'; import './index.scss'; const findFullOption = (value, options) => { - const matchedOption = options.find(option => option?.value === value); + const matchedOption = options.find((option) => option?.value === value); if (matchedOption) { if (typeof matchedOption === 'object' && matchedOption.label && matchedOption.value) { diff --git a/src/client/components/forms/field-types/index.js b/src/client/components/forms/field-types/index.js index 49a833cbb..fc431cb59 100644 --- a/src/client/components/forms/field-types/index.js +++ b/src/client/components/forms/field-types/index.js @@ -15,8 +15,8 @@ export { default as checkbox } from './Checkbox'; export { default as richText } from './RichText'; export { default as radio } from './RadioGroup'; -export { default as flexible } from './Flexible'; +export { default as blocks } from './Blocks'; export { default as group } from './Group'; -export { default as repeater } from './Repeater'; +export { default as array } from './Array'; export { default as row } from './Row'; export { default as upload } from './Upload'; diff --git a/src/client/components/forms/useFieldType/index.js b/src/client/components/forms/useFieldType/index.js index 6504fc8bc..afc8f0296 100644 --- a/src/client/components/forms/useFieldType/index.js +++ b/src/client/components/forms/useFieldType/index.js @@ -77,7 +77,7 @@ const useFieldType = (options) => { }, [setModified, modified]); // Remove field from state on "unmount" - // This is mostly used for repeater / flex content row modifications + // This is mostly used for array / flex content row modifications useUnmountEffect(() => { formContext.dispatchFields({ path, type: 'REMOVE' }); }); diff --git a/src/client/components/views/collections/List/Cell.js b/src/client/components/views/collections/List/Cell.js index 0ff206190..59644c491 100644 --- a/src/client/components/views/collections/List/Cell.js +++ b/src/client/components/views/collections/List/Cell.js @@ -63,10 +63,10 @@ const DefaultCell = (props) => { return ( {field.type !== 'date' && ( - <> + {typeof cellData === 'string' && cellData} {typeof cellData === 'object' && JSON.stringify(cellData)} - + )} ); diff --git a/src/fields/performFieldOperations.js b/src/fields/performFieldOperations.js index 82f4e6e59..279461553 100644 --- a/src/fields/performFieldOperations.js +++ b/src/fields/performFieldOperations.js @@ -72,7 +72,7 @@ module.exports = async (config, operation) => { if (field.fields) { if (field.name === undefined) { traverseFields(field.fields, data, originalDoc, path); - } else if (field.type === 'repeater' || field.type === 'flexible') { + } else if (field.type === 'array' || field.type === 'blocks') { if (Array.isArray(data[field.name])) { data[field.name].forEach((rowData, i) => { const originalDocRow = originalDoc && originalDoc[field.name] && originalDoc[field.name][i]; @@ -85,7 +85,7 @@ module.exports = async (config, operation) => { } if (operationName === 'create' || (operationName === 'update' && data[field.name] !== undefined)) { - if (field.type === 'repeater' || field.type === 'flexible') { + if (field.type === 'array' || field.type === 'blocks') { const hasRowsOfData = Array.isArray(data[field.name]); const rowCount = hasRowsOfData ? data[field.name].length : 0; diff --git a/src/fields/validations.js b/src/fields/validations.js index 208999b4a..5884cc5ec 100644 --- a/src/fields/validations.js +++ b/src/fields/validations.js @@ -128,7 +128,7 @@ const optionsToValidatorMap = { if (value || !options.required) return true; return defaultMessage; }, - repeater: (value, options = {}) => { + array: (value, options = {}) => { if (options.minRows && value < options.minRows) { return `This field requires at least ${options.minRows} row(s).`; } @@ -151,7 +151,7 @@ const optionsToValidatorMap = { if (value || !options.required) return true; return defaultMessage; }, - flexible: (value, options) => { + blocks: (value, options) => { if (options.minRows && value < options.minRows) { return `This field requires at least ${options.minRows} row(s).`; } diff --git a/src/graphql/schema/buildMutationInputType.js b/src/graphql/schema/buildMutationInputType.js index 7137b8a82..b8ab7d30f 100644 --- a/src/graphql/schema/buildMutationInputType.js +++ b/src/graphql/schema/buildMutationInputType.js @@ -16,17 +16,17 @@ const combineParentName = require('../utilities/combineParentName'); function buildMutationInputType(name, fields, parentName) { const fieldToSchemaMap = { - number: field => ({ type: withNullableType(field, GraphQLFloat) }), - text: field => ({ type: withNullableType(field, GraphQLString) }), - email: field => ({ type: withNullableType(field, GraphQLString) }), - textarea: field => ({ type: withNullableType(field, GraphQLString) }), - richText: field => ({ type: withNullableType(field, GraphQLJSON) }), - code: field => ({ type: withNullableType(field, GraphQLString) }), - date: field => ({ type: withNullableType(field, GraphQLString) }), - upload: field => ({ type: withNullableType(field, GraphQLString) }), - 'rich-text': field => ({ type: withNullableType(field, GraphQLString) }), - html: field => ({ type: withNullableType(field, GraphQLString) }), - radio: field => ({ type: withNullableType(field, GraphQLString) }), + number: (field) => ({ type: withNullableType(field, GraphQLFloat) }), + text: (field) => ({ type: withNullableType(field, GraphQLString) }), + email: (field) => ({ type: withNullableType(field, GraphQLString) }), + textarea: (field) => ({ type: withNullableType(field, GraphQLString) }), + richText: (field) => ({ type: withNullableType(field, GraphQLJSON) }), + code: (field) => ({ type: withNullableType(field, GraphQLString) }), + date: (field) => ({ type: withNullableType(field, GraphQLString) }), + upload: (field) => ({ type: withNullableType(field, GraphQLString) }), + 'rich-text': (field) => ({ type: withNullableType(field, GraphQLString) }), + html: (field) => ({ type: withNullableType(field, GraphQLString) }), + radio: (field) => ({ type: withNullableType(field, GraphQLString) }), checkbox: () => ({ type: GraphQLBoolean }), select: (field) => { const formattedName = `${combineParentName(parentName, field.name)}_MutationInput`; @@ -87,36 +87,34 @@ function buildMutationInputType(name, fields, parentName) { return { type: field.hasMany ? new GraphQLList(type) : type }; }, - repeater: (field) => { + array: (field) => { const fullName = combineParentName(parentName, field.label); let type = buildMutationInputType(fullName, field.fields, fullName); type = new GraphQLList(withNullableType(field, type)); return { type }; }, group: (field) => { - const requiresAtLeastOneField = field.fields.some(subField => (subField.required && !subField.localized)); + const requiresAtLeastOneField = field.fields.some((subField) => (subField.required && !subField.localized)); const fullName = combineParentName(parentName, field.label); let type = buildMutationInputType(fullName, field.fields, fullName); if (requiresAtLeastOneField) type = new GraphQLNonNull(type); return { type }; }, - flexible: () => ({ type: GraphQLJSON }), - row: (field) => { - return field.fields.reduce((acc, rowField) => { - const getFieldSchema = fieldToSchemaMap[rowField.type]; + blocks: () => ({ type: GraphQLJSON }), + row: (field) => field.fields.reduce((acc, rowField) => { + const getFieldSchema = fieldToSchemaMap[rowField.type]; - if (getFieldSchema) { - const fieldSchema = getFieldSchema(rowField); + if (getFieldSchema) { + const fieldSchema = getFieldSchema(rowField); - return [ - ...acc, - fieldSchema, - ]; - } + return [ + ...acc, + fieldSchema, + ]; + } - return null; - }, []); - }, + return null; + }, []), }; const fieldTypes = fields.reduce((schema, field) => { @@ -126,12 +124,10 @@ function buildMutationInputType(name, fields, parentName) { const fieldSchema = getFieldSchema(field); if (Array.isArray(fieldSchema)) { - return fieldSchema.reduce((acc, subField, i) => { - return { - ...acc, - [field.fields[i].name]: subField, - }; - }, schema); + return fieldSchema.reduce((acc, subField, i) => ({ + ...acc, + [field.fields[i].name]: subField, + }), schema); } return { diff --git a/src/graphql/schema/buildObjectType.js b/src/graphql/schema/buildObjectType.js index db7c03d1e..c4e1f7635 100644 --- a/src/graphql/schema/buildObjectType.js +++ b/src/graphql/schema/buildObjectType.js @@ -22,16 +22,16 @@ function buildObjectType(name, fields, parentName, baseFields = {}) { const recursiveBuildObjectType = buildObjectType.bind(this); const fieldToSchemaMap = { - number: field => ({ type: withNullableType(field, GraphQLFloat) }), - text: field => ({ type: withNullableType(field, GraphQLString) }), - email: field => ({ type: withNullableType(field, GraphQLString) }), - textarea: field => ({ type: withNullableType(field, GraphQLString) }), - richText: field => ({ type: withNullableType(field, GraphQLJSON) }), - code: field => ({ type: withNullableType(field, GraphQLString) }), - date: field => ({ type: withNullableType(field, GraphQLString) }), - upload: field => ({ type: withNullableType(field, GraphQLString) }), - radio: field => ({ type: withNullableType(field, GraphQLString) }), - checkbox: field => ({ type: withNullableType(field, GraphQLBoolean) }), + number: (field) => ({ type: withNullableType(field, GraphQLFloat) }), + text: (field) => ({ type: withNullableType(field, GraphQLString) }), + email: (field) => ({ type: withNullableType(field, GraphQLString) }), + textarea: (field) => ({ type: withNullableType(field, GraphQLString) }), + richText: (field) => ({ type: withNullableType(field, GraphQLJSON) }), + code: (field) => ({ type: withNullableType(field, GraphQLString) }), + date: (field) => ({ type: withNullableType(field, GraphQLString) }), + upload: (field) => ({ type: withNullableType(field, GraphQLString) }), + radio: (field) => ({ type: withNullableType(field, GraphQLString) }), + checkbox: (field) => ({ type: withNullableType(field, GraphQLBoolean) }), select: (field) => { const fullName = combineParentName(parentName, field.name); @@ -74,9 +74,7 @@ function buildObjectType(name, fields, parentName, baseFields = {}) { let type; if (isRelatedToManyCollections) { - const types = relationTo.map((relation) => { - return this.collections[relation].graphQL.type; - }); + const types = relationTo.map((relation) => this.collections[relation].graphQL.type); type = new GraphQLUnionType({ name: relationshipName, @@ -188,12 +186,10 @@ function buildObjectType(name, fields, parentName, baseFields = {}) { } if (isRelatedToManyCollections) { - const relatedCollectionFields = relationTo.reduce((allFields, relation) => { - return [ - ...allFields, - ...this.collections[relation].config.fields, - ]; - }, []); + const relatedCollectionFields = relationTo.reduce((allFields, relation) => [ + ...allFields, + ...this.collections[relation].config.fields, + ], []); relationship.args.where = { type: this.buildWhereInputType( @@ -216,7 +212,7 @@ function buildObjectType(name, fields, parentName, baseFields = {}) { return relationship; }, - repeater: (field) => { + array: (field) => { const fullName = combineParentName(parentName, field.label); let type = recursiveBuildObjectType(fullName, field.fields, fullName); type = new GraphQLList(withNullableType(field, type)); @@ -229,7 +225,7 @@ function buildObjectType(name, fields, parentName, baseFields = {}) { return { type }; }, - flexible: (field) => { + blocks: (field) => { const blockTypes = field.blocks.map((block) => { this.buildBlockType(block); return this.types.blockTypes[block.slug]; @@ -238,27 +234,23 @@ function buildObjectType(name, fields, parentName, baseFields = {}) { const type = new GraphQLList(new GraphQLUnionType({ name: combineParentName(parentName, field.label), types: blockTypes, - resolveType: (data) => { - return this.types.blockTypes[data.blockType]; - }, + resolveType: (data) => this.types.blockTypes[data.blockType], })); return { type }; }, - row: (field) => { - return field.fields.reduce((subFieldSchema, subField) => { - const buildSchemaType = fieldToSchemaMap[subField.type]; + row: (field) => field.fields.reduce((subFieldSchema, subField) => { + const buildSchemaType = fieldToSchemaMap[subField.type]; - if (buildSchemaType) { - return { - ...subFieldSchema, - [formatName(subField.name)]: buildSchemaType(subField), - }; - } + if (buildSchemaType) { + return { + ...subFieldSchema, + [formatName(subField.name)]: buildSchemaType(subField), + }; + } - return subFieldSchema; - }, {}); - }, + return subFieldSchema; + }, {}), }; const objectSchema = { diff --git a/src/graphql/schema/buildWhereInputType.js b/src/graphql/schema/buildWhereInputType.js index c7415668b..3205ee279 100644 --- a/src/graphql/schema/buildWhereInputType.js +++ b/src/graphql/schema/buildWhereInputType.js @@ -152,7 +152,7 @@ const buildWhereInputType = (name, fields, parentName) => { type, }; }, - checkbox: field => ({ + checkbox: (field) => ({ type: withOperators( field.name, GraphQLBoolean, @@ -160,7 +160,7 @@ const buildWhereInputType = (name, fields, parentName) => { ['equals', 'not_equals'], ), }), - select: field => ({ + select: (field) => ({ type: withOperators( field.name, new GraphQLEnumType({ @@ -191,34 +191,32 @@ const buildWhereInputType = (name, fields, parentName) => { ['in', 'not_in', 'all', 'equals', 'not_equals'], ), }), - repeater: field => recursivelyBuildNestedPaths(field), - group: field => recursivelyBuildNestedPaths(field), - row: (field) => { - return field.fields.reduce((rowSchema, rowField) => { - const getFieldSchema = fieldToSchemaMap[rowField.type]; + array: (field) => recursivelyBuildNestedPaths(field), + group: (field) => recursivelyBuildNestedPaths(field), + row: (field) => field.fields.reduce((rowSchema, rowField) => { + const getFieldSchema = fieldToSchemaMap[rowField.type]; - if (getFieldSchema) { - const rowFieldSchema = getFieldSchema(rowField); - - if (Array.isArray(rowFieldSchema)) { - return [ - ...rowSchema, - ...rowFieldSchema, - ]; - } + if (getFieldSchema) { + const rowFieldSchema = getFieldSchema(rowField); + if (Array.isArray(rowFieldSchema)) { return [ ...rowSchema, - { - key: rowField.name, - type: rowFieldSchema, - }, + ...rowFieldSchema, ]; } - return rowSchema; - }, []); - }, + return [ + ...rowSchema, + { + key: rowField.name, + type: rowFieldSchema, + }, + ]; + } + + return rowSchema; + }, []), }; const fieldTypes = fields.reduce((schema, field) => { @@ -230,12 +228,10 @@ const buildWhereInputType = (name, fields, parentName) => { if (Array.isArray(fieldSchema)) { return { ...schema, - ...(fieldSchema.reduce((subFields, subField) => { - return { - ...subFields, - [formatName(subField.key)]: subField.type, - }; - }, {})), + ...(fieldSchema.reduce((subFields, subField) => ({ + ...subFields, + [formatName(subField.key)]: subField.type, + }), {})), }; } diff --git a/src/localization/plugin.js b/src/localization/plugin.js index e0ca4e87f..6c38eb024 100644 --- a/src/localization/plugin.js +++ b/src/localization/plugin.js @@ -34,9 +34,7 @@ module.exports = function localizationPlugin(schema, options) { // schema.remove removes path from paths object only, but doesn't update tree // sounds like a bug, removing item from the tree manually - const tree = pathArray.reduce((mem, part) => { - return mem[part]; - }, schema.tree); + const tree = pathArray.reduce((mem, part) => mem[part], schema.tree); delete tree[key]; schema.virtual(path) @@ -103,7 +101,7 @@ module.exports = function localizationPlugin(schema, options) { }; options.locales.forEach(function (locale) { - const localeOptions = Object.assign({}, schemaType.options); + const localeOptions = { ...schemaType.options }; if (locale !== options.defaultLocale) { delete localeOptions.default; delete localeOptions.required; @@ -150,7 +148,7 @@ module.exports = function localizationPlugin(schema, options) { this.fallbackLocale = sanitizeFallbackLocale(fallbackLocale); this.schema.eachPath((path, schemaType) => { if (schemaType.options.type instanceof Array) { - if (this[path]) this[path].forEach(doc => doc.setLocale && doc.setLocale(locale, this.fallbackLocale)); + if (this[path]) this[path].forEach((doc) => doc.setLocale && doc.setLocale(locale, this.fallbackLocale)); } if (schemaType.options.ref && this[path]) { diff --git a/src/mongoose/buildQuery.js b/src/mongoose/buildQuery.js index 2583302c0..72087dee6 100644 --- a/src/mongoose/buildQuery.js +++ b/src/mongoose/buildQuery.js @@ -161,7 +161,7 @@ class ParamParser { const matchingSubDocuments = await subModel.find(subQuery); return [localizedPath, { - $in: matchingSubDocuments.map(subDoc => subDoc.id), + $in: matchingSubDocuments.map((subDoc) => subDoc.id), }]; } } diff --git a/src/mongoose/buildSchema.js b/src/mongoose/buildSchema.js index 480971ef7..636e0f58e 100644 --- a/src/mongoose/buildSchema.js +++ b/src/mongoose/buildSchema.js @@ -28,7 +28,7 @@ const buildSchema = (configFields, options = {}) => { const schema = new Schema(fields, options); configFields.forEach((field) => { - if (field.type === 'flexible' && field.blocks && field.blocks.length > 0) { + if (field.type === 'blocks' && field.blocks && field.blocks.length > 0) { field.blocks.forEach((block) => { let blockSchemaFields = {}; @@ -140,7 +140,7 @@ const fieldToSchemaMap = { return newFields; }, - repeater: (field, fields) => { + array: (field, fields) => { const schema = buildSchema(field.fields, { _id: false, id: false }); return { @@ -182,7 +182,7 @@ const fieldToSchemaMap = { [field.name]: field.hasMany ? [schema] : schema, }; }, - flexible: (field, fields) => { + blocks: (field, fields) => { const flexibleSchema = new Schema({ blockName: String }, { discriminatorKey: 'blockType', _id: false, id: false }); return { From 4f383493c6b3adfbcc59dc2f1ae5660ebd1b866d Mon Sep 17 00:00:00 2001 From: James Date: Wed, 8 Jul 2020 11:30:26 -0400 Subject: [PATCH 029/125] linting --- .eslintrc.js | 11 +++++++++++ demo/payload.config.js | 1 - demo/server.js | 2 +- .../forms/DraggableSection/ActionPanel/index.js | 4 ++++ src/localization/plugin.js | 2 +- 5 files changed, 17 insertions(+), 3 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index a6eaf41b8..e7a5016ad 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,4 +1,15 @@ module.exports = { parser: "babel-eslint", extends: "@trbl", + rules: { + "import/no-unresolved": [ + 2, + { + ignore: [ + 'payload/config', + 'payload/unsanitizedConfig', + ] + } + ], + }, }; diff --git a/demo/payload.config.js b/demo/payload.config.js index d9e8dd482..e37d6fa1b 100644 --- a/demo/payload.config.js +++ b/demo/payload.config.js @@ -69,7 +69,6 @@ module.exports = { paths: { scss: path.resolve(__dirname, 'client/scss/overrides.scss'), }, - mongoURL: 'mongodb://localhost/payload', graphQL: { mutations: {}, queries: {}, diff --git a/demo/server.js b/demo/server.js index 52b0b97f4..ebfab49b0 100644 --- a/demo/server.js +++ b/demo/server.js @@ -11,7 +11,7 @@ const payload = new Payload({ provider: 'mock', }, secret: 'SECRET_KEY', - mongoURL: 'mongodb://localhost/payload', + mongoURL: 'mongodb://localhost/test', express: expressApp, }); diff --git a/src/client/components/forms/DraggableSection/ActionPanel/index.js b/src/client/components/forms/DraggableSection/ActionPanel/index.js index c44385ad0..7599539e8 100644 --- a/src/client/components/forms/DraggableSection/ActionPanel/index.js +++ b/src/client/components/forms/DraggableSection/ActionPanel/index.js @@ -114,6 +114,7 @@ ActionPanel.defaultProps = { verticalAlignment: 'center', blockType: null, isHovered: false, + blocks: [], }; ActionPanel.propTypes = { @@ -121,6 +122,9 @@ ActionPanel.propTypes = { addRow: PropTypes.func.isRequired, removeRow: PropTypes.func.isRequired, blockType: PropTypes.oneOf(['blocks', 'array']), + blocks: PropTypes.arrayOf( + PropTypes.shape({}), + ), verticalAlignment: PropTypes.oneOf(['top', 'center', 'sticky']), isHovered: PropTypes.bool, rowIndex: PropTypes.number.isRequired, diff --git a/src/localization/plugin.js b/src/localization/plugin.js index 6c38eb024..52a4ee1cd 100644 --- a/src/localization/plugin.js +++ b/src/localization/plugin.js @@ -44,7 +44,7 @@ module.exports = function localizationPlugin(schema, options) { const locale = owner.getLocale(); const localeSubDoc = this.$__getValue(path); - if (localeSubDoc === null || localeSubDoc === void 0) { + if (localeSubDoc === null || localeSubDoc === undefined) { return localeSubDoc; } From a0b975ae738a49eb8a9a54de3c30d59dea7fed0c Mon Sep 17 00:00:00 2001 From: James Date: Wed, 8 Jul 2020 11:48:02 -0400 Subject: [PATCH 030/125] redirects after registration of first user in admin panel --- demo/server.js | 1 + src/auth/operations/registerFirstUser.js | 5 ++- src/auth/requestHandlers/registerFirstUser.js | 3 +- .../components/elements/Status/index.js | 6 ++-- src/client/components/forms/Form/index.js | 19 +++++++++-- .../components/views/CreateFirstUser/index.js | 32 ++++++------------- .../components/views/ForgotPassword/index.js | 4 +-- .../components/views/ResetPassword/index.js | 4 +-- src/tests/globalSetup.js | 2 +- 9 files changed, 41 insertions(+), 35 deletions(-) diff --git a/demo/server.js b/demo/server.js index ebfab49b0..d82687382 100644 --- a/demo/server.js +++ b/demo/server.js @@ -1,3 +1,4 @@ +/* eslint-disable no-console */ const express = require('express'); const path = require('path'); const Payload = require('../src'); diff --git a/src/auth/operations/registerFirstUser.js b/src/auth/operations/registerFirstUser.js index dec92ec40..05cc7452c 100644 --- a/src/auth/operations/registerFirstUser.js +++ b/src/auth/operations/registerFirstUser.js @@ -54,7 +54,10 @@ const registerFirstUser = async (args) => { result = await afterRegister(options, result); } - return result; + return { + message: 'Registered successfully. Welcome to Payload!', + user: result, + }; }; module.exports = registerFirstUser; diff --git a/src/auth/requestHandlers/registerFirstUser.js b/src/auth/requestHandlers/registerFirstUser.js index 7b89179d7..23b2cba91 100644 --- a/src/auth/requestHandlers/registerFirstUser.js +++ b/src/auth/requestHandlers/registerFirstUser.js @@ -1,9 +1,10 @@ const { registerFirstUser } = require('../operations'); -const registerFirstUserHandler = config => async (req, res, next) => { +const registerFirstUserHandler = (config) => async (req, res, next) => { try { const firstUser = await registerFirstUser({ req, + res, config, collection: req.collection, data: req.body, diff --git a/src/client/components/elements/Status/index.js b/src/client/components/elements/Status/index.js index 85f839fa5..b15c7be7c 100644 --- a/src/client/components/elements/Status/index.js +++ b/src/client/components/elements/Status/index.js @@ -17,10 +17,10 @@ const StatusListProvider = ({ children }) => { const [statusList, dispatchStatus] = useReducer(reducer, []); const { pathname, state } = useLocation(); - const removeStatus = useCallback(i => dispatchStatus({ type: 'REMOVE', payload: i }), []); - const addStatus = useCallback(status => dispatchStatus({ type: 'ADD', payload: status }), []); + const removeStatus = useCallback((i) => dispatchStatus({ type: 'REMOVE', payload: i }), []); + const addStatus = useCallback((status) => dispatchStatus({ type: 'ADD', payload: status }), []); const clearStatus = useCallback(() => dispatchStatus({ type: 'CLEAR' }), []); - const replaceStatus = useCallback(status => dispatchStatus({ type: 'REPLACE', payload: status }), []); + const replaceStatus = useCallback((status) => dispatchStatus({ type: 'REPLACE', payload: status }), []); useEffect(() => { if (state && state.status) { diff --git a/src/client/components/forms/Form/index.js b/src/client/components/forms/Form/index.js index a4cba6417..40aac23fb 100644 --- a/src/client/components/forms/Form/index.js +++ b/src/client/components/forms/Form/index.js @@ -127,10 +127,23 @@ const Form = (props) => { if (typeof onSuccess === 'function') onSuccess(json); if (redirect) { - return history.push(redirect, json); - } + const destination = { + pathname: redirect, + }; - if (!disableSuccessStatus) { + if (json.message && !disableSuccessStatus) { + destination.state = { + status: [ + { + message: json.message, + type: 'success', + }, + ], + }; + } + + history.push(destination); + } else if (!disableSuccessStatus) { replaceStatus([{ message: json.message, type: 'success', diff --git a/src/client/components/views/CreateFirstUser/index.js b/src/client/components/views/CreateFirstUser/index.js index 22854347f..7e964c441 100644 --- a/src/client/components/views/CreateFirstUser/index.js +++ b/src/client/components/views/CreateFirstUser/index.js @@ -1,9 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { useHistory } from 'react-router-dom'; import config from 'payload/config'; import MinimalTemplate from '../../templates/Minimal'; -import StatusList, { useStatusList } from '../../elements/Status'; import Form from '../../forms/Form'; import RenderFields from '../../forms/RenderFields'; import * as fieldTypes from '../../forms/field-types'; @@ -16,29 +14,20 @@ const { admin: { user: userSlug }, collections, serverURL, routes: { admin, api }, } = config; -const userConfig = collections.find(collection => collection.slug === userSlug); +const userConfig = collections.find((collection) => collection.slug === userSlug); const baseClass = 'create-first-user'; const CreateFirstUser = (props) => { const { setInitialized } = props; - const { addStatus } = useStatusList(); - const { setCookieToken } = useUser(); - const history = useHistory(); + const { setToken } = useUser(); - const handleAjaxResponse = (res) => { - res.json().then((data) => { - if (data.token) { - setToken(data.token); - setInitialized(true); - history.push(`${admin}`); - } else { - addStatus({ - type: 'error', - message: 'There was a problem creating your first user.', - }); - } - }); + const onSuccess = (json) => { + if (json?.user?.token) { + setToken(json.user.token); + } + + setInitialized(true); }; const fields = [ @@ -59,11 +48,10 @@ const CreateFirstUser = (props) => {

Welcome to Payload

To begin, create your first user.

-
{ const [hasSubmitted, setHasSubmitted] = useState(false); const { user } = useUser(); - const handleAjaxResponse = (res) => { + const handleResponse = (res) => { res.json() .then(() => { setHasSubmitted(true); @@ -86,7 +86,7 @@ const ForgotPassword = () => { diff --git a/src/client/components/views/ResetPassword/index.js b/src/client/components/views/ResetPassword/index.js index 9b467060f..eff81ad1c 100644 --- a/src/client/components/views/ResetPassword/index.js +++ b/src/client/components/views/ResetPassword/index.js @@ -20,7 +20,7 @@ const ResetPassword = () => { const history = useHistory(); const { user, setToken } = useUser(); - const handleAjaxResponse = (res) => { + const handleResponse = (res) => { res.json() .then((data) => { if (data.token) { @@ -60,7 +60,7 @@ const ResetPassword = () => {
{ const data = await response.json(); - if (!data.token) { + if (!data.user || !data.user.token) { throw new Error('Failed to register first user'); } }; From 59cdbe2088bd77d76500ec1bcb44e71ce6d1c391 Mon Sep 17 00:00:00 2001 From: James Date: Wed, 8 Jul 2020 11:49:23 -0400 Subject: [PATCH 031/125] reverts back to payload db for local testing --- demo/server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demo/server.js b/demo/server.js index d82687382..519675e6f 100644 --- a/demo/server.js +++ b/demo/server.js @@ -12,7 +12,7 @@ const payload = new Payload({ provider: 'mock', }, secret: 'SECRET_KEY', - mongoURL: 'mongodb://localhost/test', + mongoURL: 'mongodb://localhost/payload', express: expressApp, }); From d5e2d42c3473cdbf0b02a5b8186d615828b9d1dc Mon Sep 17 00:00:00 2001 From: James Date: Wed, 8 Jul 2020 12:19:18 -0400 Subject: [PATCH 032/125] revises /me and logs out if inactive --- src/auth/graphql/resolvers/me.js | 4 +- src/auth/operations/me.js | 8 ++- src/client/components/Routes.js | 63 ++++++++++----------- src/client/components/data/User.js | 15 ++++- src/client/components/views/Logout/index.js | 20 ++++++- src/collections/graphql/init.js | 61 +++++++++++--------- 6 files changed, 100 insertions(+), 71 deletions(-) diff --git a/src/auth/graphql/resolvers/me.js b/src/auth/graphql/resolvers/me.js index 74a4380ec..5f696334f 100644 --- a/src/auth/graphql/resolvers/me.js +++ b/src/auth/graphql/resolvers/me.js @@ -1,7 +1,5 @@ const { me } = require('../../operations'); -const meResolver = config => async (_, __, context) => { - return me({ req: context, config }); -}; +const meResolver = (config) => async (_, __, context) => me({ req: context, config }); module.exports = meResolver; diff --git a/src/auth/operations/me.js b/src/auth/operations/me.js index b5ade8b6d..b1e310664 100644 --- a/src/auth/operations/me.js +++ b/src/auth/operations/me.js @@ -5,14 +5,18 @@ const me = async ({ req, config }) => { const extractJWT = getExtractJWT(config); if (req.user) { - const response = req.user; + const response = { + user: req.user, + }; const token = extractJWT(req); if (token) { + response.token = token; + const decoded = jwt.decode(token); if (decoded) { - response.exp = decoded.exp; + response.user.exp = decoded.exp; } } diff --git a/src/client/components/Routes.js b/src/client/components/Routes.js index 1a95546d5..c61a8f3e8 100644 --- a/src/client/components/Routes.js +++ b/src/client/components/Routes.js @@ -29,7 +29,7 @@ const Routes = () => { const { user, permissions, permissions: { canAccessAdmin } } = useUser(); useEffect(() => { - requests.get(`${routes.api}/${userSlug}/init`).then(res => res.json().then((data) => { + requests.get(`${routes.api}/${userSlug}/init`).then((res) => res.json().then((data) => { if (data && 'initialized' in data) { setInitialized(data.initialized); } @@ -62,6 +62,9 @@ const Routes = () => { + + + @@ -94,14 +97,12 @@ const Routes = () => { key={`${collection.slug}-list`} path={`${match.url}/collections/${collection.slug}`} exact - render={(routeProps) => { - return ( - - ); - }} + render={(routeProps) => ( + + )} /> ); } @@ -116,14 +117,12 @@ const Routes = () => { key={`${collection.slug}-create`} path={`${match.url}/collections/${collection.slug}/create`} exact - render={(routeProps) => { - return ( - - ); - }} + render={(routeProps) => ( + + )} /> ); } @@ -138,15 +137,13 @@ const Routes = () => { key={`${collection.slug}-edit`} path={`${match.url}/collections/${collection.slug}/:id`} exact - render={(routeProps) => { - return ( - - ); - }} + render={(routeProps) => ( + + )} /> ); } @@ -161,14 +158,12 @@ const Routes = () => { key={`${global.slug}`} path={`${match.url}/globals/${global.slug}`} exact - render={(routeProps) => { - return ( - - ); - }} + render={(routeProps) => ( + + )} /> ); } diff --git a/src/client/components/data/User.js b/src/client/components/data/User.js index b0fd17076..81db999f2 100644 --- a/src/client/components/data/User.js +++ b/src/client/components/data/User.js @@ -45,9 +45,11 @@ const UserProvider = ({ children }) => { if (request.status === 200) { const json = await request.json(); setUser(json.user); + } else { + history.push(`${admin}/logout-inactivity`); } }, 1000); - }, [setUser]); + }, [setUser, history]); const setToken = useCallback((token) => { const decoded = jwt.decode(token); @@ -68,12 +70,19 @@ const UserProvider = ({ children }) => { if (request.status === 200) { const json = await request.json(); - setUser(json); + + if (json.user) { + setUser(json.user); + } + + if (json.token) { + setToken(json.token); + } } }; fetchMe(); - }, []); + }, [setToken]); // When location changes, refresh cookie useEffect(() => { diff --git a/src/client/components/views/Logout/index.js b/src/client/components/views/Logout/index.js index 0e799f2fa..a821ac28e 100644 --- a/src/client/components/views/Logout/index.js +++ b/src/client/components/views/Logout/index.js @@ -1,4 +1,5 @@ import React, { useEffect } from 'react'; +import PropTypes from 'prop-types'; import config from 'payload/config'; import { useUser } from '../../data/User'; import Minimal from '../../templates/Minimal'; @@ -10,7 +11,9 @@ const { routes: { admin } } = config; const baseClass = 'logout'; -const Logout = () => { +const Logout = (props) => { + const { inactivity } = props; + const { logOut } = useUser(); useEffect(() => { @@ -20,7 +23,12 @@ const Logout = () => { return (
-

You have been logged out successfully.

+ {inactivity && ( +

You have been logged out due to inactivity.

+ )} + {!inactivity && ( +

You have been logged out successfully.

+ )}
@@ -92,10 +92,10 @@ const DefaultEditView = (props) => { {isEditing ? (
    {permissions?.create?.permission && ( - <> +
  • Create New
  • - +
    )} {permissions?.delete?.permission && (
  • @@ -132,13 +132,13 @@ const DefaultEditView = (props) => {
)} {!isLoading && ( - <> +
field.position === 'sidebar'} + filter={(field) => field.position === 'sidebar'} position="sidebar" fieldTypes={fieldTypes} fieldSchema={fields} @@ -153,7 +153,7 @@ const DefaultEditView = (props) => {
{id}
{timestamps && ( - <> + {data.updatedAt && (
  • Last Modified
    @@ -166,12 +166,12 @@ const DefaultEditView = (props) => {
    {format(new Date(data.createdAt), 'MMMM do yyyy, h:mma')}
  • )} - +
    )} )} - + )}
    diff --git a/src/client/scss/vars.scss b/src/client/scss/vars.scss index aebc565b0..4c95fd8b4 100644 --- a/src/client/scss/vars.scss +++ b/src/client/scss/vars.scss @@ -29,6 +29,7 @@ $baseline : ($baseline-px / $baseline-body-size) + rem; ////////////////////////////// $font-body : 'Suisse Intl'; +$font-mono : monospace; ////////////////////////////// // COLORS From 1be29a0aa06d63ebead8767b2d003402989af580 Mon Sep 17 00:00:00 2001 From: James Date: Thu, 9 Jul 2020 19:56:17 -0400 Subject: [PATCH 040/125] fixes bug with default column specification --- .../elements/ColumnSelector/getInitialState.js | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/client/components/elements/ColumnSelector/getInitialState.js b/src/client/components/elements/ColumnSelector/getInitialState.js index a7ec04dc3..fd394973c 100644 --- a/src/client/components/elements/ColumnSelector/getInitialState.js +++ b/src/client/components/elements/ColumnSelector/getInitialState.js @@ -1,10 +1,12 @@ const getInitialColumnState = (fields, useAsTitle, defaultColumns) => { let initialColumns = []; - const hasThumbnail = fields.find(field => field.type === 'thumbnail'); + const hasThumbnail = fields.find((field) => field.type === 'thumbnail'); - if (Array.isArray(defaultColumns)) { - initialColumns = defaultColumns; + if (Array.isArray(defaultColumns) && defaultColumns.length >= 1) { + return { + columns: defaultColumns, + }; } if (hasThumbnail) { @@ -15,11 +17,8 @@ const getInitialColumnState = (fields, useAsTitle, defaultColumns) => { initialColumns.push(useAsTitle); } - const remainingColumns = fields.filter((field) => { - return field.name !== useAsTitle && field.type !== 'thumbnail'; - }).slice(0, 3 - initialColumns.length).map((field) => { - return field.name; - }); + const remainingColumns = fields.filter((field) => field.name !== useAsTitle && field.type !== 'thumbnail') + .slice(0, 3 - initialColumns.length).map((field) => field.name); initialColumns = initialColumns.concat(remainingColumns); From 3ce886b6654eccaa8a5b8c18fcb0d98052c618d3 Mon Sep 17 00:00:00 2001 From: James Date: Thu, 9 Jul 2020 19:56:38 -0400 Subject: [PATCH 041/125] implements permissions within Dashboard view --- .../components/views/Dashboard/Default.js | 75 +++++++++++-------- 1 file changed, 43 insertions(+), 32 deletions(-) diff --git a/src/client/components/views/Dashboard/Default.js b/src/client/components/views/Dashboard/Default.js index 5d070b42f..50bd6b44f 100644 --- a/src/client/components/views/Dashboard/Default.js +++ b/src/client/components/views/Dashboard/Default.js @@ -1,6 +1,7 @@ -import React, { useEffect } from 'react'; +import React, { useEffect, useState } from 'react'; import { useHistory } from 'react-router-dom'; import config from 'payload/config'; +import { useUser } from '../../data/User'; import { useStepNav } from '../../elements/StepNav'; import Eyebrow from '../../elements/Eyebrow'; import Card from '../../elements/Card'; @@ -19,8 +20,16 @@ const { const baseClass = 'dashboard'; const Dashboard = () => { + const [filteredGlobals, setFilteredGlobals] = useState([]); const { setStepNav } = useStepNav(); const { push } = useHistory(); + const { permissions } = useUser(); + + useEffect(() => { + setFilteredGlobals( + globals.filter((global) => permissions?.[global.slug]?.read?.permission), + ); + }, [permissions]); useEffect(() => { setStepNav([]); @@ -33,42 +42,44 @@ const Dashboard = () => {

    Collections

      {collections.map((collection) => { - return ( -
    • - push({ pathname: `${admin}/collections/${collection.slug}` })} - actions={( -
    • - ); + if (permissions?.[collection.slug]?.read?.permission) { + return ( +
    • + push({ pathname: `${admin}/collections/${collection.slug}` })} + actions={( +
    • + ); + } + + return null; })}
    - {(globals && globals.length > 0) && ( - <> + {(filteredGlobals.length > 0) && ( +

    Globals

      - {globals.map((global) => { - return ( -
    • - push({ pathname: `${admin}/globals/${global.slug}` })} - /> -
    • - ); - })} + {filteredGlobals.map((global) => ( +
    • + push({ pathname: `${admin}/globals/${global.slug}` })} + /> +
    • + ))}
    - +
    )}
    From 41373f5e1689d084823982434696ecb888ca7738 Mon Sep 17 00:00:00 2001 From: James Date: Thu, 9 Jul 2020 20:09:57 -0400 Subject: [PATCH 042/125] displays error if relationship options fail to load --- .../forms/field-types/Relationship/index.js | 111 +++++++++++------- .../forms/field-types/Relationship/index.scss | 8 ++ 2 files changed, 76 insertions(+), 43 deletions(-) diff --git a/src/client/components/forms/field-types/Relationship/index.js b/src/client/components/forms/field-types/Relationship/index.js index aaae16cf3..fb867dc28 100644 --- a/src/client/components/forms/field-types/Relationship/index.js +++ b/src/client/components/forms/field-types/Relationship/index.js @@ -19,6 +19,8 @@ const { const maxResultsPerRequest = 10; +const baseClass = 'relationship'; + class Relationship extends Component { constructor(props) { super(props); @@ -31,6 +33,7 @@ class Relationship extends Component { lastFullyLoadedRelation: -1, lastLoadedPage: 1, options: [], + errorLoading: false, }; } @@ -46,6 +49,7 @@ class Relationship extends Component { } getNextOptions = (params = {}) => { + const { errorLoading } = this.state; const { clear } = params; if (clear) { @@ -54,42 +58,55 @@ class Relationship extends Component { }); } - const { - relations, lastFullyLoadedRelation, lastLoadedPage, search, - } = this.state; + if (!errorLoading) { + const { + relations, lastFullyLoadedRelation, lastLoadedPage, search, + } = this.state; - const relationsToSearch = relations.slice(lastFullyLoadedRelation + 1); + const relationsToSearch = relations.slice(lastFullyLoadedRelation + 1); - if (relationsToSearch.length > 0) { - some(relationsToSearch, async (relation, callback) => { - const collection = collections.find((coll) => coll.slug === relation); - const fieldToSearch = collection.useAsTitle || 'id'; - const searchParam = search ? `&where[${fieldToSearch}][like]=${search}` : ''; - const response = await fetch(`${serverURL}${api}/${relation}?limit=${maxResultsPerRequest}&page=${lastLoadedPage}${searchParam}`); + if (relationsToSearch.length > 0) { + some(relationsToSearch, async (relation, callback) => { + const collection = collections.find((coll) => coll.slug === relation); + const fieldToSearch = collection.useAsTitle || 'id'; + const searchParam = search ? `&where[${fieldToSearch}][like]=${search}` : ''; + const response = await fetch(`${serverURL}${api}/${relation}?limit=${maxResultsPerRequest}&page=${lastLoadedPage}${searchParam}`); + const data = await response.json(); - const data = await response.json(); + if (response.ok) { + if (data.hasNextPage) { + return callback(false, { + data, + relation, + }); + } - if (data.hasNextPage) { - return callback(false, { - data, - relation, - }); - } + return callback({ relation, data }); + } + + let error = 'There was a problem loading options for this field.'; + + if (response.status === 403) { + error = 'You do not have permission to load options for this field.'; + } - return callback({ relation, data }); - }, (lastPage, nextPage) => { - if (nextPage) { - const { data, relation } = nextPage; - this.addOptions(data, relation); - } else { - const { data, relation } = lastPage; - this.addOptions(data, relation); this.setState({ - lastFullyLoadedRelation: relations.indexOf(relation), - lastLoadedPage: 1, + errorLoading: error, }); - } - }); + }, (lastPage, nextPage) => { + if (nextPage) { + const { data, relation } = nextPage; + this.addOptions(data, relation); + } else { + const { data, relation } = lastPage; + this.addOptions(data, relation); + this.setState({ + lastFullyLoadedRelation: relations.indexOf(relation), + lastLoadedPage: 1, + }); + } + }); + } } } @@ -199,7 +216,7 @@ class Relationship extends Component { } render() { - const { options } = this.state; + const { options, errorLoading } = this.state; const { path, @@ -218,8 +235,9 @@ class Relationship extends Component { const classes = [ 'field-type', - 'relationship', + baseClass, showError && 'error', + errorLoading && 'error-loading', readOnly && 'read-only', ].filter(Boolean).join(' '); @@ -246,18 +264,25 @@ class Relationship extends Component { label={label} required={required} /> - + {!errorLoading && ( + + )} + {errorLoading && ( +
    + {errorLoading} +
    + )}
    ); } diff --git a/src/client/components/forms/field-types/Relationship/index.scss b/src/client/components/forms/field-types/Relationship/index.scss index 0d3871c1e..9d9b280ae 100644 --- a/src/client/components/forms/field-types/Relationship/index.scss +++ b/src/client/components/forms/field-types/Relationship/index.scss @@ -3,3 +3,11 @@ .field-type.relationship { position: relative; } + +.relationship__error-loading { + border: 1px solid $color-red; + height: base(2); + padding: base(.5) base(.75); + background-color: $color-red; + color: white; +} From 26f4e7303e92d230cbba3420605b48889bb467f4 Mon Sep 17 00:00:00 2001 From: James Date: Thu, 9 Jul 2020 21:58:11 -0400 Subject: [PATCH 043/125] passes res to graphql context in order to login via gql --- src/auth/graphql/resolvers/access.js | 4 ++-- src/auth/graphql/resolvers/forgotPassword.js | 2 +- src/auth/graphql/resolvers/init.js | 2 +- src/auth/graphql/resolvers/login.js | 3 ++- src/auth/graphql/resolvers/me.js | 2 +- src/auth/graphql/resolvers/refresh.js | 3 ++- src/auth/graphql/resolvers/register.js | 6 +++--- src/auth/graphql/resolvers/resetPassword.js | 7 +++---- src/auth/graphql/resolvers/update.js | 6 +++--- src/collections/graphql/resolvers/create.js | 6 +++--- src/collections/graphql/resolvers/delete.js | 8 ++++---- src/collections/graphql/resolvers/find.js | 6 +++--- src/collections/graphql/resolvers/findByID.js | 6 +++--- src/collections/graphql/resolvers/update.js | 8 ++++---- src/globals/graphql/resolvers/findOne.js | 6 +++--- src/globals/graphql/resolvers/update.js | 6 +++--- src/graphql/index.js | 5 ++++- src/index.js | 2 +- 18 files changed, 46 insertions(+), 42 deletions(-) diff --git a/src/auth/graphql/resolvers/access.js b/src/auth/graphql/resolvers/access.js index 8b207178a..d42d3fc17 100644 --- a/src/auth/graphql/resolvers/access.js +++ b/src/auth/graphql/resolvers/access.js @@ -13,10 +13,10 @@ const formatConfigNames = (results, configs) => { return formattedResults; }; -const policyResolver = config => async (_, __, context) => { +const policyResolver = (config) => async (_, __, context) => { const options = { config, - req: context, + req: context.req, }; let policyResults = await access(options); diff --git a/src/auth/graphql/resolvers/forgotPassword.js b/src/auth/graphql/resolvers/forgotPassword.js index 418b2f303..b874bafd7 100644 --- a/src/auth/graphql/resolvers/forgotPassword.js +++ b/src/auth/graphql/resolvers/forgotPassword.js @@ -7,7 +7,7 @@ const forgotPasswordResolver = (config, collection, email) => async (_, args, co collection, data: args, email, - req: context, + req: context.req, }; await forgotPassword(options); diff --git a/src/auth/graphql/resolvers/init.js b/src/auth/graphql/resolvers/init.js index 5a4ad5060..9a49564f8 100644 --- a/src/auth/graphql/resolvers/init.js +++ b/src/auth/graphql/resolvers/init.js @@ -4,7 +4,7 @@ const { init } = require('../../operations'); const initResolver = ({ Model }) => async (_, __, context) => { const options = { Model, - req: context, + req: context.req, }; const result = await init(options); diff --git a/src/auth/graphql/resolvers/login.js b/src/auth/graphql/resolvers/login.js index d5cce96c9..ad61ba4be 100644 --- a/src/auth/graphql/resolvers/login.js +++ b/src/auth/graphql/resolvers/login.js @@ -9,7 +9,8 @@ const loginResolver = (config, collection) => async (_, args, context) => { email: args.email, password: args.password, }, - req: context, + req: context.req, + res: context.res, }; const token = await login(options); diff --git a/src/auth/graphql/resolvers/me.js b/src/auth/graphql/resolvers/me.js index 5f696334f..da09ce897 100644 --- a/src/auth/graphql/resolvers/me.js +++ b/src/auth/graphql/resolvers/me.js @@ -1,5 +1,5 @@ const { me } = require('../../operations'); -const meResolver = (config) => async (_, __, context) => me({ req: context, config }); +const meResolver = (config) => async (_, __, context) => me({ req: context.req, config }); module.exports = meResolver; diff --git a/src/auth/graphql/resolvers/refresh.js b/src/auth/graphql/resolvers/refresh.js index 28041d91b..b382fc899 100644 --- a/src/auth/graphql/resolvers/refresh.js +++ b/src/auth/graphql/resolvers/refresh.js @@ -10,7 +10,8 @@ const refreshResolver = (config, collection) => async (_, __, context) => { config, collection, token, - req: context, + req: context.req, + res: context.res, }; const result = await refresh(options); diff --git a/src/auth/graphql/resolvers/register.js b/src/auth/graphql/resolvers/register.js index ca160ad7a..8a601e91a 100644 --- a/src/auth/graphql/resolvers/register.js +++ b/src/auth/graphql/resolvers/register.js @@ -7,16 +7,16 @@ const registerResolver = (config, collection) => async (_, args, context) => { collection, data: args.data, depth: 0, - req: context, + req: context.req, }; if (args.locale) { - context.locale = args.locale; + context.req.locale = args.locale; options.locale = args.locale; } if (args.fallbackLocale) { - context.fallbackLocale = args.fallbackLocale; + context.req.fallbackLocale = args.fallbackLocale; options.fallbackLocale = args.fallbackLocale; } diff --git a/src/auth/graphql/resolvers/resetPassword.js b/src/auth/graphql/resolvers/resetPassword.js index ff9662814..23e264a53 100644 --- a/src/auth/graphql/resolvers/resetPassword.js +++ b/src/auth/graphql/resolvers/resetPassword.js @@ -2,16 +2,15 @@ const { resetPassword } = require('../../operations'); const resetPasswordResolver = (config, collection) => async (_, args, context) => { - if (args.locale) context.locale = args.locale; - if (args.fallbackLocale) context.fallbackLocale = args.fallbackLocale; + if (args.locale) context.req.locale = args.locale; + if (args.fallbackLocale) context.req.fallbackLocale = args.fallbackLocale; const options = { collection, config, data: args, - req: context, + req: context.req, api: 'GraphQL', - user: context.user, }; const token = await resetPassword(options); diff --git a/src/auth/graphql/resolvers/update.js b/src/auth/graphql/resolvers/update.js index 2d917fccc..34b64e6b0 100644 --- a/src/auth/graphql/resolvers/update.js +++ b/src/auth/graphql/resolvers/update.js @@ -2,8 +2,8 @@ const { update } = require('../../operations'); const updateResolver = ({ Model, config }) => async (_, args, context) => { - if (args.locale) context.locale = args.locale; - if (args.fallbackLocale) context.fallbackLocale = args.fallbackLocale; + if (args.locale) context.req.locale = args.locale; + if (args.fallbackLocale) context.req.fallbackLocale = args.fallbackLocale; const options = { config, @@ -11,7 +11,7 @@ const updateResolver = ({ Model, config }) => async (_, args, context) => { data: args.data, id: args.id, depth: 0, - req: context, + req: context.req, }; const user = await update(options); diff --git a/src/collections/graphql/resolvers/create.js b/src/collections/graphql/resolvers/create.js index 9ddda6b38..46ae1f919 100644 --- a/src/collections/graphql/resolvers/create.js +++ b/src/collections/graphql/resolvers/create.js @@ -1,16 +1,16 @@ /* eslint-disable no-param-reassign */ const { create } = require('../../operations'); -const createResolver = collection => async (_, args, context) => { +const createResolver = (collection) => async (_, args, context) => { if (args.locale) { - context.locale = args.locale; + context.req.locale = args.locale; } const options = { config: collection.config, Model: collection.Model, data: args.data, - req: context, + req: context.req, }; const result = await create(options); diff --git a/src/collections/graphql/resolvers/delete.js b/src/collections/graphql/resolvers/delete.js index 1a820bfa1..e272450fa 100644 --- a/src/collections/graphql/resolvers/delete.js +++ b/src/collections/graphql/resolvers/delete.js @@ -1,15 +1,15 @@ /* eslint-disable no-param-reassign */ const { deleteQuery } = require('../../operations'); -const deleteResolver = collection => async (_, args, context) => { - if (args.locale) context.locale = args.locale; - if (args.fallbackLocale) context.fallbackLocale = args.fallbackLocale; +const deleteResolver = (collection) => async (_, args, context) => { + if (args.locale) context.req.locale = args.locale; + if (args.fallbackLocale) context.req.fallbackLocale = args.fallbackLocale; const options = { config: collection.config, Model: collection.Model, id: args.id, - req: context, + req: context.req, }; const result = await deleteQuery(options); diff --git a/src/collections/graphql/resolvers/find.js b/src/collections/graphql/resolvers/find.js index 52b614e68..e87194e2d 100644 --- a/src/collections/graphql/resolvers/find.js +++ b/src/collections/graphql/resolvers/find.js @@ -2,8 +2,8 @@ const { find } = require('../../operations'); const findResolver = (collection) => async (_, args, context) => { - if (args.locale) context.locale = args.locale; - if (args.fallbackLocale) context.fallbackLocale = args.fallbackLocale; + if (args.locale) context.req.locale = args.locale; + if (args.fallbackLocale) context.req.fallbackLocale = args.fallbackLocale; const options = { config: collection.config, @@ -12,7 +12,7 @@ const findResolver = (collection) => async (_, args, context) => { limit: args.limit, page: args.page, sort: args.sort, - req: context, + req: context.req, }; const results = await find(options); diff --git a/src/collections/graphql/resolvers/findByID.js b/src/collections/graphql/resolvers/findByID.js index afa705ff7..16298a0c1 100644 --- a/src/collections/graphql/resolvers/findByID.js +++ b/src/collections/graphql/resolvers/findByID.js @@ -2,14 +2,14 @@ const { findByID } = require('../../operations'); const findByIDResolver = (collection) => async (_, args, context) => { - if (args.locale) context.locale = args.locale; - if (args.fallbackLocale) context.fallbackLocale = args.fallbackLocale; + if (args.locale) context.req.locale = args.locale; + if (args.fallbackLocale) context.req.fallbackLocale = args.fallbackLocale; const options = { config: collection.config, Model: collection.Model, id: args.id, - req: context, + req: context.req, }; const result = await findByID(options); diff --git a/src/collections/graphql/resolvers/update.js b/src/collections/graphql/resolvers/update.js index 73bc33c99..60e94e3df 100644 --- a/src/collections/graphql/resolvers/update.js +++ b/src/collections/graphql/resolvers/update.js @@ -1,9 +1,9 @@ /* eslint-disable no-param-reassign */ const { update } = require('../../operations'); -const updateResolver = collection => async (_, args, context) => { - if (args.locale) context.locale = args.locale; - if (args.fallbackLocale) context.fallbackLocale = args.fallbackLocale; +const updateResolver = (collection) => async (_, args, context) => { + if (args.locale) context.req.locale = args.locale; + if (args.fallbackLocale) context.req.fallbackLocale = args.fallbackLocale; const options = { config: collection.config, @@ -11,7 +11,7 @@ const updateResolver = collection => async (_, args, context) => { data: args.data, id: args.id, depth: 0, - req: context, + req: context.req, }; const result = await update(options); diff --git a/src/globals/graphql/resolvers/findOne.js b/src/globals/graphql/resolvers/findOne.js index 6f31fda15..852d532e9 100644 --- a/src/globals/graphql/resolvers/findOne.js +++ b/src/globals/graphql/resolvers/findOne.js @@ -2,8 +2,8 @@ const { findOne } = require('../../operations'); const findOneResolver = (Model, config) => async (_, args, context) => { - if (args.locale) context.locale = args.locale; - if (args.fallbackLocale) context.fallbackLocale = args.fallbackLocale; + if (args.locale) context.req.locale = args.locale; + if (args.fallbackLocale) context.req.fallbackLocale = args.fallbackLocale; const { slug } = config; @@ -12,7 +12,7 @@ const findOneResolver = (Model, config) => async (_, args, context) => { config, slug, depth: 0, - req: context, + req: context.req, }; const result = await findOne(options); diff --git a/src/globals/graphql/resolvers/update.js b/src/globals/graphql/resolvers/update.js index 31f251223..4fe99ee38 100644 --- a/src/globals/graphql/resolvers/update.js +++ b/src/globals/graphql/resolvers/update.js @@ -2,8 +2,8 @@ const { update } = require('../../operations'); const updateResolver = (Model, config) => async (_, args, context) => { - if (args.locale) context.locale = args.locale; - if (args.fallbackLocale) context.fallbackLocale = args.fallbackLocale; + if (args.locale) context.req.locale = args.locale; + if (args.fallbackLocale) context.req.fallbackLocale = args.fallbackLocale; const { slug } = config; @@ -13,7 +13,7 @@ const updateResolver = (Model, config) => async (_, args, context) => { data: args.data, slug, depth: 0, - req: context, + req: context.req, }; const result = await update(options); diff --git a/src/graphql/index.js b/src/graphql/index.js index d5f4256f5..2df06f8df 100644 --- a/src/graphql/index.js +++ b/src/graphql/index.js @@ -14,9 +14,11 @@ const errorHandler = require('./errorHandler'); const { access } = require('../auth/graphql/resolvers'); class GraphQL { - constructor(init) { + constructor(init, req, res) { Object.assign(this, init); this.init = this.init.bind(this); + this.req = req; + this.res = res; this.types = { blockTypes: {}, @@ -94,6 +96,7 @@ class GraphQL { return response; }, extensions, + context: { req: this.req, res: this.res }, }); } } diff --git a/src/index.js b/src/index.js index e53b4e4ee..6b3dfdb50 100644 --- a/src/index.js +++ b/src/index.js @@ -58,7 +58,7 @@ class Payload { this.router.use( this.config.routes.graphQL, identifyAPI('GraphQL'), - new GraphQL(this).init(), + (req, res) => new GraphQL(this, req, res).init()(req, res), ); this.router.get(this.config.routes.graphQLPlayground, graphQLPlayground({ From 419a6c547c8b58893537120d2f412e4d4d47033a Mon Sep 17 00:00:00 2001 From: James Date: Fri, 10 Jul 2020 01:59:03 -0400 Subject: [PATCH 044/125] enables access to auth / setting cookie in graphql resolvers, secures relationships --- demo/collections/Hooks.js | 28 +++-- demo/server.js | 2 +- src/auth/getExecuteStaticAccess.js | 12 +- src/auth/getExecuteStaticPolicy.js | 40 ------- src/auth/graphql/resolvers/access.js | 12 +- src/auth/graphql/resolvers/index.js | 2 + src/auth/graphql/resolvers/logout.js | 16 +++ src/auth/graphql/resolvers/update.js | 4 +- src/auth/operations/access.js | 24 +++- src/auth/operations/index.js | 2 + src/auth/operations/logout.js | 26 +++++ src/auth/operations/register.js | 63 +++++++---- src/auth/operations/registerFirstUser.js | 38 ++----- src/auth/operations/update.js | 79 ++++++++----- src/auth/requestHandlers/access.js | 6 +- src/auth/requestHandlers/logout.js | 27 ++--- src/auth/requestHandlers/register.js | 2 +- src/auth/requestHandlers/update.js | 6 +- src/auth/routes.js | 8 +- src/collections/graphql/init.js | 15 ++- src/collections/graphql/resolvers/create.js | 6 +- src/collections/graphql/resolvers/delete.js | 3 +- src/collections/graphql/resolvers/find.js | 6 +- src/collections/graphql/resolvers/findByID.js | 6 +- src/collections/graphql/resolvers/update.js | 6 +- src/collections/init.js | 2 +- src/collections/operations/create.js | 82 ++++++++------ src/collections/operations/delete.js | 52 ++++----- src/collections/operations/find.js | 82 +++++++------- src/collections/operations/findByID.js | 64 ++++++----- src/collections/operations/update.js | 107 +++++++++++------- .../requestHandlers/collections.spec.js | 20 +--- src/collections/requestHandlers/create.js | 6 +- src/collections/requestHandlers/delete.js | 6 +- src/collections/requestHandlers/find.js | 6 +- src/collections/requestHandlers/findByID.js | 6 +- src/collections/requestHandlers/update.js | 6 +- src/collections/routes.js | 18 +-- src/fields/performFieldOperations.js | 38 +++++-- src/globals/graphql/init.js | 4 +- src/globals/graphql/resolvers/findOne.js | 5 +- src/globals/graphql/resolvers/update.js | 3 +- src/globals/init.js | 2 +- src/globals/operations/findOne.js | 73 ++++++------ src/globals/operations/update.js | 62 ++++++---- src/globals/requestHandlers/findOne.js | 3 +- src/globals/requestHandlers/update.js | 3 +- src/globals/routes.js | 8 +- src/mongoose/buildSchema.js | 4 +- 49 files changed, 613 insertions(+), 488 deletions(-) delete mode 100644 src/auth/getExecuteStaticPolicy.js create mode 100644 src/auth/graphql/resolvers/logout.js create mode 100644 src/auth/operations/logout.js diff --git a/demo/collections/Hooks.js b/demo/collections/Hooks.js index 925531d97..b5d5cf779 100644 --- a/demo/collections/Hooks.js +++ b/demo/collections/Hooks.js @@ -17,32 +17,30 @@ module.exports = { if (operation.req.headers.hook === 'beforeCreate') { operation.req.body.description += '-beforeCreateSuffix'; } - return operation; + return operation.data; }, beforeRead: (operation) => { if (operation.req.headers.hook === 'beforeRead') { - operation.limit = 1; + console.log('before reading Hooks document'); } - return operation; }, beforeUpdate: (operation) => { if (operation.req.headers.hook === 'beforeUpdate') { operation.req.body.description += '-beforeUpdateSuffix'; } - return operation; + return operation.data; }, beforeDelete: (operation) => { if (operation.req.headers.hook === 'beforeDelete') { // TODO: Find a better hook operation to assert against in tests operation.req.headers.hook = 'afterDelete'; } - return operation; }, - afterCreate: (operation, value) => { + afterCreate: (operation) => { if (operation.req.headers.hook === 'afterCreate') { - value.afterCreateHook = true; + operation.doc.afterCreateHook = true; } - return value; + return operation.doc; }, afterRead: (operation) => { const { doc } = operation; @@ -50,17 +48,17 @@ module.exports = { return doc; }, - afterUpdate: (operation, value) => { + afterUpdate: (operation) => { if (operation.req.headers.hook === 'afterUpdate') { - value.afterUpdateHook = true; + operation.doc.afterUpdateHook = true; } - return value; + return operation.doc; }, - afterDelete: (operation, value) => { + afterDelete: (operation) => { if (operation.req.headers.hook === 'afterDelete') { - value.afterDeleteHook = true; + operation.doc.afterDeleteHook = true; } - return value; + return operation.doc; }, }, fields: [ @@ -73,7 +71,7 @@ module.exports = { unique: true, localized: true, hooks: { - afterRead: value => (value ? value.toUpperCase() : null), + afterRead: (value) => (value ? value.toUpperCase() : null), }, }, { diff --git a/demo/server.js b/demo/server.js index e5a01693f..0d79a33a6 100644 --- a/demo/server.js +++ b/demo/server.js @@ -15,7 +15,7 @@ const payload = new Payload({ mongoURL: 'mongodb://localhost/payload', express: expressApp, onInit: () => { - console.log('Payload is started'); + console.log('Payload is initialized'); }, }); diff --git a/src/auth/getExecuteStaticAccess.js b/src/auth/getExecuteStaticAccess.js index 7467cb168..3f8d64fd9 100644 --- a/src/auth/getExecuteStaticAccess.js +++ b/src/auth/getExecuteStaticAccess.js @@ -1,12 +1,12 @@ -const executeStatic = require('./executeAccess'); +const executeAccess = require('./executeAccess'); const { Forbidden } = require('../errors'); -const getExecuteStaticPolicy = ({ config, Model }) => async (req, res, next) => { +const getExecuteStaticAccess = ({ config, Model }) => async (req, res, next) => { try { if (req.path) { - const policyResult = await executeStatic({ req, isReadingStaticFile: true }, config.access.read); + const accessResult = await executeAccess({ req, isReadingStaticFile: true }, config.access.read); - if (typeof policyResult === 'object') { + if (typeof accessResult === 'object') { const filename = decodeURI(req.path).replace(/^\/|\/$/g, ''); const queryToBuild = { @@ -17,7 +17,7 @@ const getExecuteStaticPolicy = ({ config, Model }) => async (req, res, next) => equals: filename, }, }, - policyResult, + accessResult, ], }, }; @@ -37,4 +37,4 @@ const getExecuteStaticPolicy = ({ config, Model }) => async (req, res, next) => } }; -module.exports = getExecuteStaticPolicy; +module.exports = getExecuteStaticAccess; diff --git a/src/auth/getExecuteStaticPolicy.js b/src/auth/getExecuteStaticPolicy.js deleted file mode 100644 index 0403a25b3..000000000 --- a/src/auth/getExecuteStaticPolicy.js +++ /dev/null @@ -1,40 +0,0 @@ -const executePolicy = require('./executeAccess'); -const { Forbidden } = require('../errors'); - -const getExecuteStaticPolicy = ({ config, Model }) => async (req, res, next) => { - try { - if (req.path) { - const policyResult = await executePolicy({ req, isReadingStaticFile: true }, config.policies.read); - - if (typeof policyResult === 'object') { - const filename = decodeURI(req.path).replace(/^\/|\/$/g, ''); - - const queryToBuild = { - where: { - and: [ - { - filename: { - equals: filename, - }, - }, - policyResult, - ], - }, - }; - - const query = await Model.buildQuery(queryToBuild, req.locale); - const doc = await Model.findOne(query); - - if (!doc) { - throw new Forbidden(); - } - } - } - - return next(); - } catch (error) { - return next(error); - } -}; - -module.exports = getExecuteStaticPolicy; diff --git a/src/auth/graphql/resolvers/access.js b/src/auth/graphql/resolvers/access.js index d42d3fc17..da4be370c 100644 --- a/src/auth/graphql/resolvers/access.js +++ b/src/auth/graphql/resolvers/access.js @@ -13,18 +13,18 @@ const formatConfigNames = (results, configs) => { return formattedResults; }; -const policyResolver = (config) => async (_, __, context) => { +const accessResolver = (config) => async (_, __, context) => { const options = { config, req: context.req, }; - let policyResults = await access(options); + let accessResults = await access(options); - policyResults = formatConfigNames(policyResults, config.collections); - policyResults = formatConfigNames(policyResults, config.globals); + accessResults = formatConfigNames(accessResults, config.collections); + accessResults = formatConfigNames(accessResults, config.globals); - return policyResults; + return accessResults; }; -module.exports = policyResolver; +module.exports = accessResolver; diff --git a/src/auth/graphql/resolvers/index.js b/src/auth/graphql/resolvers/index.js index 1a5be0daf..9347af057 100644 --- a/src/auth/graphql/resolvers/index.js +++ b/src/auth/graphql/resolvers/index.js @@ -7,6 +7,7 @@ const forgotPassword = require('./forgotPassword'); const resetPassword = require('./resetPassword'); const update = require('./update'); const access = require('./access'); +const logout = require('./logout'); module.exports = { login, @@ -18,4 +19,5 @@ module.exports = { resetPassword, update, access, + logout, }; diff --git a/src/auth/graphql/resolvers/logout.js b/src/auth/graphql/resolvers/logout.js new file mode 100644 index 000000000..6fdc2909c --- /dev/null +++ b/src/auth/graphql/resolvers/logout.js @@ -0,0 +1,16 @@ +/* eslint-disable no-param-reassign */ +const { logout } = require('../../operations'); + +const logoutResolver = (config, collection) => async (_, __, context) => { + const options = { + config, + collection, + res: context.res, + }; + + const result = await logout(options); + + return result; +}; + +module.exports = logoutResolver; diff --git a/src/auth/graphql/resolvers/update.js b/src/auth/graphql/resolvers/update.js index 34b64e6b0..d19bef3c5 100644 --- a/src/auth/graphql/resolvers/update.js +++ b/src/auth/graphql/resolvers/update.js @@ -1,13 +1,13 @@ /* eslint-disable no-param-reassign */ const { update } = require('../../operations'); -const updateResolver = ({ Model, config }) => async (_, args, context) => { +const updateResolver = (config, collection) => async (_, args, context) => { if (args.locale) context.req.locale = args.locale; if (args.fallbackLocale) context.req.fallbackLocale = args.fallbackLocale; const options = { config, - Model, + collection, data: args.data, id: args.id, depth: 0, diff --git a/src/auth/operations/access.js b/src/auth/operations/access.js index 5ced85042..3e460d744 100644 --- a/src/auth/operations/access.js +++ b/src/auth/operations/access.js @@ -1,6 +1,6 @@ const allOperations = ['create', 'read', 'update', 'delete']; -const access = async (args) => { +const accessOperation = async (args) => { const { config, req, @@ -13,7 +13,7 @@ const access = async (args) => { const isLoggedIn = !!(user); const userCollectionConfig = (user && user.collection) ? config.collections.find((collection) => collection.slug === user.collection) : null; - const createPolicyPromise = async (obj, access, operation, disableWhere = false) => { + const createAccessPromise = async (obj, access, operation, disableWhere = false) => { const updatedObj = obj; const result = await access({ req }); @@ -32,18 +32,30 @@ const access = async (args) => { const executeFieldPolicies = (obj, fields, operation) => { const updatedObj = obj; - fields.forEach((field) => { + fields.forEach(async (field) => { if (field.name) { if (!updatedObj[field.name]) updatedObj[field.name] = {}; if (field.access && typeof field.access[operation] === 'function') { - promises.push(createPolicyPromise(updatedObj[field.name], field.access[operation], operation, true)); + promises.push(createAccessPromise(updatedObj[field.name], field.access[operation], operation, true)); } else { updatedObj[field.name][operation] = { permission: isLoggedIn, }; } + if (field.type === 'relationship') { + const relatedCollections = Array.isArray(field.relationTo) ? field.relationTo : [field.relationTo]; + + relatedCollections.forEach((slug) => { + const collection = config.collections.find((coll) => coll.slug === slug); + + if (collection && collection.access && collection.access[operation]) { + promises.push(createAccessPromise(updatedObj[field.name], collection.access[operation], operation, true)); + } + }); + } + if (field.fields) { if (!updatedObj[field.name].fields) updatedObj[field.name].fields = {}; executeFieldPolicies(updatedObj[field.name].fields, field.fields, operation); @@ -63,7 +75,7 @@ const access = async (args) => { executeFieldPolicies(results[entity.slug].fields, entity.fields, operation); if (typeof entity.access[operation] === 'function') { - promises.push(createPolicyPromise(results[entity.slug], entity.access[operation], operation)); + promises.push(createAccessPromise(results[entity.slug], entity.access[operation], operation)); } else { results[entity.slug][operation] = { permission: isLoggedIn, @@ -91,4 +103,4 @@ const access = async (args) => { return results; }; -module.exports = access; +module.exports = accessOperation; diff --git a/src/auth/operations/index.js b/src/auth/operations/index.js index 0abb26674..f9da1162a 100644 --- a/src/auth/operations/index.js +++ b/src/auth/operations/index.js @@ -8,6 +8,7 @@ const registerFirstUser = require('./registerFirstUser'); const update = require('./update'); const access = require('./access'); const me = require('./me'); +const logout = require('./logout'); module.exports = { login, @@ -20,4 +21,5 @@ module.exports = { registerFirstUser, access, me, + logout, }; diff --git a/src/auth/operations/logout.js b/src/auth/operations/logout.js new file mode 100644 index 000000000..721eb1afd --- /dev/null +++ b/src/auth/operations/logout.js @@ -0,0 +1,26 @@ +const logout = async (args) => { + const { + config, + collection: { + config: collectionConfig, + }, + res, + } = args; + + const cookieOptions = { + expires: new Date(0), + httpOnly: true, + path: '/', + overwrite: true, + }; + + if (collectionConfig.auth && collectionConfig.auth.secureCookie) { + cookieOptions.secure = true; + } + + res.cookie(`${config.cookiePrefix}-token`, '', cookieOptions); + + return 'Logged out successfully.'; +}; + +module.exports = logout; diff --git a/src/auth/operations/register.js b/src/auth/operations/register.js index 265e5d0ed..12c4aa556 100644 --- a/src/auth/operations/register.js +++ b/src/auth/operations/register.js @@ -1,49 +1,60 @@ const passport = require('passport'); -const executeStatic = require('../executeAccess'); +const executeAccess = require('../executeAccess'); const performFieldOperations = require('../../fields/performFieldOperations'); const register = async (args) => { + const { + overrideAccess, + config, + collection: { + Model, + config: collectionConfig, + }, + req, + req: { + locale, + fallbackLocale, + }, + } = args; + + let { data } = args; + // ///////////////////////////////////// // 1. Retrieve and execute access // ///////////////////////////////////// - if (!args.overridePolicy) { - await executeStatic(args, args.collection.config.access.create); + if (!overrideAccess) { + await executeAccess({ req }, collectionConfig.access.create); } - let options = { ...args }; - // ///////////////////////////////////// // 2. Execute before register hook // ///////////////////////////////////// - const { beforeRegister } = args.collection.config.hooks; + const { beforeRegister } = collectionConfig.hooks; if (typeof beforeRegister === 'function') { - options = await beforeRegister(options); + data = (await beforeRegister({ + data, + req, + })) || data; } // ///////////////////////////////////// // 3. Execute field-level hooks, access, and validation // ///////////////////////////////////// - options.data = await performFieldOperations(args.collection.config, { ...options, hook: 'beforeCreate', operationName: 'create' }); + data = await performFieldOperations(config, collectionConfig, { + data, + hook: 'beforeCreate', + operationName: 'create', + req, + }); // ///////////////////////////////////// // 6. Perform register // ///////////////////////////////////// - const { - collection: { - Model, - }, - data, - req: { - locale, - fallbackLocale, - }, - } = options; - const modelData = { ...data }; delete modelData.password; @@ -65,18 +76,24 @@ const register = async (args) => { // 7. Execute field-level hooks and access // ///////////////////////////////////// - result = await performFieldOperations(args.collection.config, { - ...options, data: result, hook: 'afterRead', operationName: 'read', + result = await performFieldOperations(config, collectionConfig, { + data: result, + hook: 'afterRead', + operationName: 'read', + req, }); // ///////////////////////////////////// // 8. Execute after register hook // ///////////////////////////////////// - const afterRegister = args.collection.config.hooks; + const { afterRegister } = collectionConfig.hooks; if (typeof afterRegister === 'function') { - result = await afterRegister(options, result); + result = (await afterRegister({ + data: result, + req: args.req, + })) || result; } // ///////////////////////////////////// diff --git a/src/auth/operations/registerFirstUser.js b/src/auth/operations/registerFirstUser.js index 05cc7452c..88c181ba0 100644 --- a/src/auth/operations/registerFirstUser.js +++ b/src/auth/operations/registerFirstUser.js @@ -3,31 +3,23 @@ const login = require('./login'); const { Forbidden } = require('../../errors'); const registerFirstUser = async (args) => { - const count = await args.collection.Model.countDocuments({}); + const { + collection: { + Model, + }, + } = args; + + const count = await Model.countDocuments({}); if (count >= 1) throw new Forbidden(); - // Await validation here - - let options = { ...args }; - - // ///////////////////////////////////// - // 1. Execute before register first user hook - // ///////////////////////////////////// - - const { beforeRegister } = args.collection.config.hooks; - - if (typeof beforeRegister === 'function') { - options = await beforeRegister(options); - } - // ///////////////////////////////////// // 2. Perform register first user // ///////////////////////////////////// let result = await register({ - ...options, - overridePolicy: true, + ...args, + overrideAccess: true, }); @@ -36,7 +28,7 @@ const registerFirstUser = async (args) => { // ///////////////////////////////////// const token = await login({ - ...options, + ...args, }); result = { @@ -44,16 +36,6 @@ const registerFirstUser = async (args) => { token, }; - // ///////////////////////////////////// - // 4. Execute after register first user hook - // ///////////////////////////////////// - - const afterRegister = args.config.hooks; - - if (typeof afterRegister === 'function') { - result = await afterRegister(options, result); - } - return { message: 'Registered successfully. Welcome to Payload!', user: result, diff --git a/src/auth/operations/update.js b/src/auth/operations/update.js index b47c8d48a..6cbc2bd42 100644 --- a/src/auth/operations/update.js +++ b/src/auth/operations/update.js @@ -1,79 +1,94 @@ const deepmerge = require('deepmerge'); const overwriteMerge = require('../../utilities/overwriteMerge'); const { NotFound, Forbidden } = require('../../errors'); -const executeStatic = require('../executeAccess'); +const executeAccess = require('../executeAccess'); const performFieldOperations = require('../../fields/performFieldOperations'); const update = async (args) => { + const { + config, + collection: { + Model, + config: collectionConfig, + }, + id, + req, + req: { + locale, + fallbackLocale, + }, + } = args; + // ///////////////////////////////////// // 1. Execute access // ///////////////////////////////////// - const policyResults = await executeStatic(args, args.config.access.update); - const hasWherePolicy = typeof policyResults === 'object'; - - let options = { ...args }; + const accessResults = await executeAccess({ req }, collectionConfig.access.update); + const hasWhereAccess = typeof accessResults === 'object'; // ///////////////////////////////////// // 2. Retrieve document // ///////////////////////////////////// - const { - Model, - id, - req: { - locale, - fallbackLocale, - }, - } = options; - let query = { _id: id }; - if (hasWherePolicy) { + if (hasWhereAccess) { query = { ...query, - ...policyResults, + ...accessResults, }; } let user = await Model.findOne(query); - if (!user && !hasWherePolicy) throw new NotFound(); - if (!user && hasWherePolicy) throw new Forbidden(); + if (!user && !hasWhereAccess) throw new NotFound(); + if (!user && hasWhereAccess) throw new Forbidden(); if (locale && user.setLocale) { user.setLocale(locale, fallbackLocale); } - const userJSON = user.toJSON({ virtuals: true }); + const originalDoc = user.toJSON({ virtuals: true }); + + let { data } = args; // ///////////////////////////////////// // 2. Execute before update hook // ///////////////////////////////////// - const { beforeUpdate } = args.config.hooks; + const { beforeUpdate } = collectionConfig.hooks; if (typeof beforeUpdate === 'function') { - options = await beforeUpdate(options); + data = (await beforeUpdate({ + data, + req, + originalDoc, + })) || data; } // ///////////////////////////////////// // 3. Merge updates into existing data // ///////////////////////////////////// - options.data = deepmerge(userJSON, options.data, { arrayMerge: overwriteMerge }); + data = deepmerge(originalDoc, data, { arrayMerge: overwriteMerge }); // ///////////////////////////////////// // 4. Execute field-level hooks, access, and validation // ///////////////////////////////////// - options.data = await performFieldOperations(args.config, { ...options, hook: 'beforeUpdate', operationName: 'update' }); + data = await performFieldOperations(config, collectionConfig, { + data, + req, + hook: 'beforeUpdate', + operationName: 'update', + originalDoc, + }); // ///////////////////////////////////// // 5. Handle password update // ///////////////////////////////////// - const dataToUpdate = { ...options.data }; + const dataToUpdate = { ...data }; const { password } = dataToUpdate; if (password) { @@ -95,18 +110,24 @@ const update = async (args) => { // 7. Execute field-level hooks and access // ///////////////////////////////////// - user = performFieldOperations(args.config, { - ...options, data: user, hook: 'afterRead', operationName: 'read', + user = performFieldOperations(config, collectionConfig, { + data: user, + hook: 'afterRead', + operationName: 'read', + req, }); // ///////////////////////////////////// // 8. Execute after update hook // ///////////////////////////////////// - const afterUpdateHook = args.config.hooks && args.config.hooks.afterUpdate; + const { afterUpdate } = collectionConfig.hooks; - if (typeof afterUpdateHook === 'function') { - user = await afterUpdateHook(options, user); + if (typeof afterUpdate === 'function') { + user = (await afterUpdate({ + data: user, + req, + })) || user; } // ///////////////////////////////////// diff --git a/src/auth/requestHandlers/access.js b/src/auth/requestHandlers/access.js index bdf71b912..e94fe8e70 100644 --- a/src/auth/requestHandlers/access.js +++ b/src/auth/requestHandlers/access.js @@ -1,15 +1,15 @@ const httpStatus = require('http-status'); const { access } = require('../operations'); -const policiesHandler = config => async (req, res, next) => { +const policiesHandler = (config) => async (req, res, next) => { try { - const policyResults = await access({ + const accessResults = await access({ req, config, }); return res.status(httpStatus.OK) - .json(policyResults); + .json(accessResults); } catch (error) { return next(error); } diff --git a/src/auth/requestHandlers/logout.js b/src/auth/requestHandlers/logout.js index 6222ffd25..6258c4250 100644 --- a/src/auth/requestHandlers/logout.js +++ b/src/auth/requestHandlers/logout.js @@ -1,22 +1,17 @@ -const logoutHandler = config => async (req, res) => { - const { collection } = req; +const { logout } = require('../operations'); - const cookieOptions = { - expires: new Date(0), - httpOnly: true, - path: '/', - overwrite: true, - }; +const logoutHandler = (config) => async (req, res, next) => { + try { + const message = await logout({ + config, + collection: req.collection, + res, + }); - if (collection.auth && collection.auth.secureCookie) { - cookieOptions.secure = true; + return res.status(200).json({ message }); + } catch (error) { + return next(error); } - - res.cookie(`${config.cookiePrefix}-token`, '', cookieOptions); - - return res.status(200).json({ - message: 'Logged out successfully.', - }); }; module.exports = logoutHandler; diff --git a/src/auth/requestHandlers/register.js b/src/auth/requestHandlers/register.js index bf7e15fa1..9757114ae 100644 --- a/src/auth/requestHandlers/register.js +++ b/src/auth/requestHandlers/register.js @@ -2,7 +2,7 @@ const httpStatus = require('http-status'); const formatSuccessResponse = require('../../express/responses/formatSuccess'); const { register } = require('../operations'); -const registerHandler = config => async (req, res, next) => { +const registerHandler = (config) => async (req, res, next) => { try { const user = await register({ config, diff --git a/src/auth/requestHandlers/update.js b/src/auth/requestHandlers/update.js index 2bdd1e702..a4927f355 100644 --- a/src/auth/requestHandlers/update.js +++ b/src/auth/requestHandlers/update.js @@ -2,13 +2,13 @@ const httpStatus = require('http-status'); const formatSuccessResponse = require('../../express/responses/formatSuccess'); const { update } = require('../operations'); -const updateHandler = async (req, res, next) => { +const updateHandler = (config) => async (req, res, next) => { try { const user = await update({ req, data: req.body, - Model: req.collection.Model, - config: req.collection.config, + collection: req.collection, + config, id: req.params.id, }); diff --git a/src/auth/routes.js b/src/auth/routes.js index fc326968a..f0a2088b3 100644 --- a/src/auth/routes.js +++ b/src/auth/routes.js @@ -66,12 +66,12 @@ const authRoutes = (collection, config, sendEmail) => { router .route(`/${slug}`) - .get(find); + .get(find(config)); router.route(`/${slug}/:id`) - .get(findByID) - .put(update) - .delete(deleteHandler); + .get(findByID(config)) + .put(update(config)) + .delete(deleteHandler(config)); return router; }; diff --git a/src/collections/graphql/init.js b/src/collections/graphql/init.js index 5f62d6d8c..46c5f1f22 100644 --- a/src/collections/graphql/init.js +++ b/src/collections/graphql/init.js @@ -13,7 +13,7 @@ const { } = require('./resolvers'); const { - login, me, init, refresh, register, forgotPassword, resetPassword, + login, logout, me, init, refresh, register, forgotPassword, resetPassword, } = require('../../auth/graphql/resolvers'); const buildPaginatedListType = require('../../graphql/schema/buildPaginatedListType'); @@ -93,7 +93,7 @@ function registerCollections() { locale: { type: this.types.localeInputType }, fallbackLocale: { type: this.types.fallbackLocaleInputType }, }, - resolve: findByID(collection), + resolve: findByID(this.config, collection), }; this.Query.fields[pluralLabel] = { @@ -106,7 +106,7 @@ function registerCollections() { limit: { type: GraphQLInt }, sort: { type: GraphQLString }, }, - resolve: find(collection), + resolve: find(this.config, collection), }; this.Mutation.fields[`update${singularLabel}`] = { @@ -115,7 +115,7 @@ function registerCollections() { id: { type: new GraphQLNonNull(GraphQLString) }, data: { type: collection.graphQL.updateMutationInputType }, }, - resolve: update(collection), + resolve: update(this.config, collection), }; this.Mutation.fields[`delete${singularLabel}`] = { @@ -174,6 +174,11 @@ function registerCollections() { resolve: login(this.config, collection), }; + this.Mutation.fields[`logout${singularLabel}`] = { + type: GraphQLString, + resolve: logout(this.config, collection), + }; + this.Mutation.fields[`register${singularLabel}`] = { type: collection.graphQL.type, args: { @@ -209,7 +214,7 @@ function registerCollections() { args: { data: { type: collection.graphQL.mutationInputType }, }, - resolve: create(collection), + resolve: create(this.config, collection), }; } }); diff --git a/src/collections/graphql/resolvers/create.js b/src/collections/graphql/resolvers/create.js index 46ae1f919..25f0d3e08 100644 --- a/src/collections/graphql/resolvers/create.js +++ b/src/collections/graphql/resolvers/create.js @@ -1,14 +1,14 @@ /* eslint-disable no-param-reassign */ const { create } = require('../../operations'); -const createResolver = (collection) => async (_, args, context) => { +const createResolver = (config, collection) => async (_, args, context) => { if (args.locale) { context.req.locale = args.locale; } const options = { - config: collection.config, - Model: collection.Model, + config, + collection, data: args.data, req: context.req, }; diff --git a/src/collections/graphql/resolvers/delete.js b/src/collections/graphql/resolvers/delete.js index e272450fa..e5899716f 100644 --- a/src/collections/graphql/resolvers/delete.js +++ b/src/collections/graphql/resolvers/delete.js @@ -6,8 +6,7 @@ const deleteResolver = (collection) => async (_, args, context) => { if (args.fallbackLocale) context.req.fallbackLocale = args.fallbackLocale; const options = { - config: collection.config, - Model: collection.Model, + collection, id: args.id, req: context.req, }; diff --git a/src/collections/graphql/resolvers/find.js b/src/collections/graphql/resolvers/find.js index e87194e2d..87d071f99 100644 --- a/src/collections/graphql/resolvers/find.js +++ b/src/collections/graphql/resolvers/find.js @@ -1,13 +1,13 @@ /* eslint-disable no-param-reassign */ const { find } = require('../../operations'); -const findResolver = (collection) => async (_, args, context) => { +const findResolver = (config, collection) => async (_, args, context) => { if (args.locale) context.req.locale = args.locale; if (args.fallbackLocale) context.req.fallbackLocale = args.fallbackLocale; const options = { - config: collection.config, - Model: collection.Model, + config, + collection, where: args.where, limit: args.limit, page: args.page, diff --git a/src/collections/graphql/resolvers/findByID.js b/src/collections/graphql/resolvers/findByID.js index 16298a0c1..880a432f1 100644 --- a/src/collections/graphql/resolvers/findByID.js +++ b/src/collections/graphql/resolvers/findByID.js @@ -1,13 +1,13 @@ /* eslint-disable no-param-reassign */ const { findByID } = require('../../operations'); -const findByIDResolver = (collection) => async (_, args, context) => { +const findByIDResolver = (config, collection) => async (_, args, context) => { if (args.locale) context.req.locale = args.locale; if (args.fallbackLocale) context.req.fallbackLocale = args.fallbackLocale; const options = { - config: collection.config, - Model: collection.Model, + config, + collection, id: args.id, req: context.req, }; diff --git a/src/collections/graphql/resolvers/update.js b/src/collections/graphql/resolvers/update.js index 60e94e3df..ec829889e 100644 --- a/src/collections/graphql/resolvers/update.js +++ b/src/collections/graphql/resolvers/update.js @@ -1,13 +1,13 @@ /* eslint-disable no-param-reassign */ const { update } = require('../../operations'); -const updateResolver = (collection) => async (_, args, context) => { +const updateResolver = (config, collection) => async (_, args, context) => { if (args.locale) context.req.locale = args.locale; if (args.fallbackLocale) context.req.fallbackLocale = args.fallbackLocale; const options = { - config: collection.config, - Model: collection.Model, + config, + collection, data: args.data, id: args.id, depth: 0, diff --git a/src/collections/init.js b/src/collections/init.js index f3ab82cdc..eb29a8b3b 100644 --- a/src/collections/init.js +++ b/src/collections/init.js @@ -40,7 +40,7 @@ function registerCollections() { this.router.use(authRoutes(AuthCollection, this.config, this.sendEmail)); } else { - this.router.use(collectionRoutes(this.collections[formattedCollection.slug])); + this.router.use(collectionRoutes(this.collections[formattedCollection.slug], this.config)); } return formattedCollection; diff --git a/src/collections/operations/create.js b/src/collections/operations/create.js index b5360a3a3..33d666680 100644 --- a/src/collections/operations/create.js +++ b/src/collections/operations/create.js @@ -1,6 +1,6 @@ const mkdirp = require('mkdirp'); -const executeStatic = require('../../auth/executeAccess'); +const executeAccess = require('../../auth/executeAccess'); const { MissingFile } = require('../../errors'); const resizeAndSave = require('../../uploads/imageResizer'); @@ -11,65 +11,86 @@ const imageMIMETypes = require('../../uploads/imageMIMETypes'); const performFieldOperations = require('../../fields/performFieldOperations'); const create = async (args) => { + const { + collection: { + Model, + config: collectionConfig, + }, + req, + req: { + locale, + fallbackLocale, + }, + config, + } = args; + + let { data } = args; + // ///////////////////////////////////// // 1. Retrieve and execute access // ///////////////////////////////////// - await executeStatic(args, args.config.access.create); - - let options = { ...args }; + await executeAccess({ req }, collectionConfig.access.create); // ///////////////////////////////////// // 2. Execute before collection hook // ///////////////////////////////////// - const { beforeCreate } = args.config.hooks; + const { beforeCreate } = collectionConfig.hooks; if (typeof beforeCreate === 'function') { - options = await beforeCreate(options); + data = (await beforeCreate({ + data, + req, + })) || data; } // ///////////////////////////////////// // 3. Execute field-level access, hooks, and validation // ///////////////////////////////////// - options.data = await performFieldOperations(args.config, { ...options, hook: 'beforeCreate', operationName: 'create' }); + data = await performFieldOperations(config, collectionConfig, { + data, + hook: 'beforeCreate', + operationName: 'create', + req, + }); // ///////////////////////////////////// // 4. Upload and resize any files that may be present // ///////////////////////////////////// - if (args.config.upload) { - const { staticDir, imageSizes } = options.req.collection.config.upload; + if (collectionConfig.upload) { + const { staticDir, imageSizes } = collectionConfig.upload; const fileData = {}; - if (!args.req.files || Object.keys(args.req.files).length === 0) { + if (!req.files || Object.keys(req.files).length === 0) { throw new MissingFile(); } await mkdirp(staticDir); - const fsSafeName = await getSafeFilename(staticDir, options.req.files.file.name); + const fsSafeName = await getSafeFilename(staticDir, req.files.file.name); - await options.req.files.file.mv(`${staticDir}/${fsSafeName}`); + await req.files.file.mv(`${staticDir}/${fsSafeName}`); - if (imageMIMETypes.indexOf(options.req.files.file.mimetype) > -1) { + if (imageMIMETypes.indexOf(req.files.file.mimetype) > -1) { const dimensions = await getImageSize(`${staticDir}/${fsSafeName}`); fileData.width = dimensions.width; fileData.height = dimensions.height; - if (Array.isArray(imageSizes) && options.req.files.file.mimetype !== 'image/svg+xml') { - fileData.sizes = await resizeAndSave(options.config, fsSafeName, fileData.mimeType); + if (Array.isArray(imageSizes) && req.files.file.mimetype !== 'image/svg+xml') { + fileData.sizes = await resizeAndSave(collectionConfig, fsSafeName, fileData.mimeType); } } fileData.filename = fsSafeName; - fileData.filesize = options.req.files.file.size; - fileData.mimeType = options.req.files.file.mimetype; + fileData.filesize = req.files.file.size; + fileData.mimeType = req.files.file.mimetype; - options.data = { - ...options.data, + data = { + ...data, ...fileData, }; } @@ -78,15 +99,6 @@ const create = async (args) => { // 5. Perform database operation // ///////////////////////////////////// - const { - Model, - data, - req: { - locale, - fallbackLocale, - }, - } = options; - let result = new Model(); if (locale && result.setLocale) { @@ -102,18 +114,24 @@ const create = async (args) => { // 6. Execute field-level hooks and access // ///////////////////////////////////// - result = await performFieldOperations(args.config, { - ...options, data: result, hook: 'afterRead', operationName: 'read', + result = await performFieldOperations(config, collectionConfig, { + data: result, + hook: 'afterRead', + operationName: 'read', + req, }); // ///////////////////////////////////// // 7. Execute after collection hook // ///////////////////////////////////// - const { afterCreate } = args.config.hooks; + const { afterCreate } = collectionConfig.hooks; if (typeof afterCreate === 'function') { - result = await afterCreate(options, result); + result = await afterCreate({ + doc: result, + req: args.req, + }) || result; } // ///////////////////////////////////// diff --git a/src/collections/operations/delete.js b/src/collections/operations/delete.js index 62d7960fb..53bb24005 100644 --- a/src/collections/operations/delete.js +++ b/src/collections/operations/delete.js @@ -1,55 +1,55 @@ const fs = require('fs'); const { NotFound, Forbidden, ErrorDeletingFile } = require('../../errors'); -const executeStatic = require('../../auth/executeAccess'); +const executeAccess = require('../../auth/executeAccess'); const deleteQuery = async (args) => { + const { + collection: { + Model, + config: collectionConfig, + }, + id, + req, + req: { + locale, + fallbackLocale, + }, + } = args; + // ///////////////////////////////////// // 1. Retrieve and execute access // ///////////////////////////////////// - const policyResults = await executeStatic(args, args.config.access.delete); - const hasWherePolicy = typeof policyResults === 'object'; - - let options = { - ...args, - }; + const accessResults = await executeAccess({ req, id }, collectionConfig.access.delete); + const hasWhereAccess = typeof accessResults === 'object'; // ///////////////////////////////////// // 2. Execute before collection hook // ///////////////////////////////////// - const { beforeDelete } = args.config.hooks; + const { beforeDelete } = collectionConfig.hooks; if (typeof beforeDelete === 'function') { - options = await beforeDelete(options); + await beforeDelete({ req, id }); } // ///////////////////////////////////// // 3. Get existing document // ///////////////////////////////////// - const { - Model, - id, - req: { - locale, - fallbackLocale, - }, - } = options; - let query = { _id: id }; - if (hasWherePolicy) { + if (hasWhereAccess) { query = { ...query, - ...policyResults, + ...accessResults, }; } let resultToDelete = await Model.findOne(query); - if (!resultToDelete && !hasWherePolicy) throw new NotFound(); - if (!resultToDelete && hasWherePolicy) throw new Forbidden(); + if (!resultToDelete && !hasWhereAccess) throw new NotFound(); + if (!resultToDelete && hasWhereAccess) throw new Forbidden(); resultToDelete = resultToDelete.toJSON({ virtuals: true }); @@ -61,8 +61,8 @@ const deleteQuery = async (args) => { // 4. Delete any associated files // ///////////////////////////////////// - if (options.req.collection.config.upload) { - const { staticDir } = options.req.collection.config.upload; + if (collectionConfig.upload) { + const { staticDir } = collectionConfig.upload; fs.unlink(`${staticDir}/${resultToDelete.filename}`, () => { throw new ErrorDeletingFile(); @@ -93,10 +93,10 @@ const deleteQuery = async (args) => { // 4. Execute after collection hook // ///////////////////////////////////// - const { afterDelete } = args.config.hooks; + const { afterDelete } = collectionConfig.hooks; if (typeof afterDelete === 'function') { - result = await afterDelete(options, result) || result; + result = await afterDelete({ req, id, doc: result }) || result; } // ///////////////////////////////////// diff --git a/src/collections/operations/find.js b/src/collections/operations/find.js index c134e371c..7b6fa0672 100644 --- a/src/collections/operations/find.js +++ b/src/collections/operations/find.js @@ -1,71 +1,72 @@ -const executeStatic = require('../../auth/executeAccess'); +const executeAccess = require('../../auth/executeAccess'); const performFieldOperations = require('../../fields/performFieldOperations'); const find = async (args) => { + const { + where, + page, + limit, + depth, + config, + collection: { + Model, + config: collectionConfig, + }, + req, + req: { + locale, + fallbackLocale, + payloadAPI, + }, + } = args; + // ///////////////////////////////////// // 1. Retrieve and execute access // ///////////////////////////////////// - const policyResults = await executeStatic(args, args.config.access.read); - const hasWherePolicy = typeof policyResults === 'object'; + const accessResults = await executeAccess({ req }, collectionConfig.access.read); + const hasWhereAccess = typeof accessResults === 'object'; const queryToBuild = {}; - if (args.where) { + if (where) { queryToBuild.where = { - and: [args.where], + and: [where], }; } - if (hasWherePolicy) { - if (!args.where) { + if (hasWhereAccess) { + if (!where) { queryToBuild.where = { and: [ - policyResults, + accessResults, ], }; } else { - queryToBuild.where.and.push(policyResults); + queryToBuild.where.and.push(accessResults); } } - let options = { - ...args, - query: await args.Model.buildQuery(queryToBuild, args.req.locale), - }; + const query = await Model.buildQuery(queryToBuild, locale); // ///////////////////////////////////// // 2. Execute before collection hook // ///////////////////////////////////// - const { beforeRead } = args.config.hooks; + const { beforeRead } = collectionConfig.hooks; if (typeof beforeRead === 'function') { - options = await beforeRead(options); + await beforeRead({ req, query }); } // ///////////////////////////////////// // 3. Perform database operation // ///////////////////////////////////// - const { - query, - page, - limit, - depth, - Model, - req: { - locale, - fallbackLocale, - payloadAPI, - }, - config, - } = options; - - let { sort } = options; + let { sort } = args; if (!sort) { - if (config.timestamps) { + if (collectionConfig.timestamps) { sort = '-createdAt'; } else { sort = '-_id'; @@ -108,8 +109,11 @@ const find = async (args) => { const data = doc.toJSON({ virtuals: true }); - return performFieldOperations(args.config, { - ...options, data, hook: 'afterRead', operationName: 'read', + return performFieldOperations(config, collectionConfig, { + data, + req, + hook: 'afterRead', + operationName: 'read', }); })), }; @@ -118,14 +122,16 @@ const find = async (args) => { // 6. Execute afterRead collection hook // ///////////////////////////////////// - const { afterRead } = args.config.hooks; - let afterReadResult = null; + const { afterRead } = collectionConfig.hooks; + + let afterReadResult = result; if (typeof afterRead === 'function') { afterReadResult = { ...result, - docs: await Promise.all(result.docs.map(async (doc) => afterRead({ - options, + docs: await Promise.all(result.docs.map(async (doc) => await afterRead({ + req, + query, doc, }) || doc)), }; @@ -135,7 +141,7 @@ const find = async (args) => { // 7. Return results // ///////////////////////////////////// - return afterReadResult || result; + return afterReadResult; }; module.exports = find; diff --git a/src/collections/operations/findByID.js b/src/collections/operations/findByID.js index 338beaf1e..cc96150da 100644 --- a/src/collections/operations/findByID.js +++ b/src/collections/operations/findByID.js @@ -1,61 +1,63 @@ const { Forbidden, NotFound } = require('../../errors'); -const executeStatic = require('../../auth/executeAccess'); +const executeAccess = require('../../auth/executeAccess'); const performFieldOperations = require('../../fields/performFieldOperations'); const findByID = async (args) => { + const { + config, + depth, + collection: { + Model, + config: collectionConfig, + }, + id, + req, + req: { + locale, + fallbackLocale, + payloadAPI, + }, + } = args; + // ///////////////////////////////////// // 1. Retrieve and execute access // ///////////////////////////////////// - const policyResults = await executeStatic(args, args.config.access.read); - const hasWherePolicy = typeof policyResults === 'object'; + const accessResults = await executeAccess({ req }, collectionConfig.access.read); + const hasWhereAccess = typeof accessResults === 'object'; const queryToBuild = { where: { and: [ { _id: { - equals: args.id, + equals: id, }, }, ], }, }; - if (hasWherePolicy) { - queryToBuild.where.and.push(policyResults); + if (hasWhereAccess) { + queryToBuild.where.and.push(accessResults); } - let options = { - ...args, - query: await args.Model.buildQuery(queryToBuild, args.req.locale), - }; + const query = await Model.buildQuery(queryToBuild, locale); // ///////////////////////////////////// // 2. Execute before collection hook // ///////////////////////////////////// - const { beforeRead } = args.config.hooks; + const { beforeRead } = collectionConfig.hooks; if (typeof beforeRead === 'function') { - options = await beforeRead(options); + await beforeRead({ req, query }); } // ///////////////////////////////////// // 3. Perform database operation // ///////////////////////////////////// - const { - depth, - Model, - query, - req: { - locale, - fallbackLocale, - payloadAPI, - }, - } = options; - const queryOptionsToExecute = { options: { autopopulate: false, @@ -75,8 +77,8 @@ const findByID = async (args) => { let result = await Model.findOne(query, {}, queryOptionsToExecute); - if (!result && !hasWherePolicy) throw new NotFound(); - if (!result && hasWherePolicy) throw new Forbidden(); + if (!result && !hasWhereAccess) throw new NotFound(); + if (!result && hasWhereAccess) throw new Forbidden(); if (locale && result.setLocale) { result.setLocale(locale, fallbackLocale); @@ -88,8 +90,11 @@ const findByID = async (args) => { // 4. Execute field-level hooks and access // ///////////////////////////////////// - result = await performFieldOperations(args.config, { - ...options, data: result, hook: 'afterRead', operationName: 'read', + result = await performFieldOperations(config, collectionConfig, { + req, + data: result, + hook: 'afterRead', + operationName: 'read', }); @@ -97,11 +102,12 @@ const findByID = async (args) => { // 5. Execute after collection hook // ///////////////////////////////////// - const { afterRead } = args.config.hooks; + const { afterRead } = collectionConfig.hooks; if (typeof afterRead === 'function') { result = await afterRead({ - ...options, + req, + query, doc: result, }) || result; } diff --git a/src/collections/operations/update.js b/src/collections/operations/update.js index cc0558b67..98d2c7556 100644 --- a/src/collections/operations/update.js +++ b/src/collections/operations/update.js @@ -1,6 +1,6 @@ const deepmerge = require('deepmerge'); const overwriteMerge = require('../../utilities/overwriteMerge'); -const executeStatic = require('../../auth/executeAccess'); +const executeAccess = require('../../auth/executeAccess'); const { NotFound, Forbidden } = require('../../errors'); const performFieldOperations = require('../../fields/performFieldOperations'); const imageMIMETypes = require('../../uploads/imageMIMETypes'); @@ -10,27 +10,30 @@ const getSafeFilename = require('../../uploads/getSafeFilename'); const resizeAndSave = require('../../uploads/imageResizer'); const update = async (args) => { - // ///////////////////////////////////// - // 1. Execute access - // ///////////////////////////////////// - - const policyResults = await executeStatic(args, args.config.access.update); - const hasWherePolicy = typeof policyResults === 'object'; - - let options = { ...args }; - - // ///////////////////////////////////// - // 2. Retrieve document - // ///////////////////////////////////// - const { - Model, + config, + collection: { + Model, + config: collectionConfig, + }, id, + req, req: { locale, fallbackLocale, }, - } = options; + } = args; + + // ///////////////////////////////////// + // 1. Execute access + // ///////////////////////////////////// + + const policyResults = await executeAccess({ req }, collectionConfig.access.update); + const hasWherePolicy = typeof policyResults === 'object'; + + // ///////////////////////////////////// + // 2. Retrieve document + // ///////////////////////////////////// const queryToBuild = { where: { @@ -48,9 +51,9 @@ const update = async (args) => { queryToBuild.where.and.push(hasWherePolicy); } - options.query = await args.Model.buildQuery(queryToBuild, locale); + const query = await Model.buildQuery(queryToBuild, locale); - let doc = await Model.findOne(options.query); + let doc = await Model.findOne(query); if (!doc && !hasWherePolicy) throw new NotFound(); if (!doc && hasWherePolicy) throw new Forbidden(); @@ -59,65 +62,77 @@ const update = async (args) => { doc.setLocale(locale, fallbackLocale); } - options.originalDoc = doc.toJSON({ virtuals: true }); + const originalDoc = doc.toJSON({ virtuals: true }); // ///////////////////////////////////// // 2. Execute before update hook // ///////////////////////////////////// - const { beforeUpdate } = args.config.hooks; + let { data } = args; + + const { beforeUpdate } = collectionConfig.hooks; if (typeof beforeUpdate === 'function') { - options = await beforeUpdate(options); + data = await beforeUpdate({ + data, + req, + originalDoc, + }) || data; } // ///////////////////////////////////// // 3. Merge updates into existing data // ///////////////////////////////////// - options.data = deepmerge(options.originalDoc, options.data, { arrayMerge: overwriteMerge }); + data = deepmerge(originalDoc, data, { arrayMerge: overwriteMerge }); // ///////////////////////////////////// // 4. Execute field-level hooks, access, and validation // ///////////////////////////////////// - options.data = await performFieldOperations(args.config, { ...options, hook: 'beforeUpdate', operationName: 'update' }); + data = await performFieldOperations(config, collectionConfig, { + data, + req, + originalDoc, + hook: 'beforeUpdate', + operationName: 'update', + }); // ///////////////////////////////////// // 5. Upload and resize any files that may be present // ///////////////////////////////////// - if (args.config.upload) { + if (collectionConfig.upload) { const fileData = {}; - const { staticDir, imageSizes } = args.config.upload; + const { staticDir, imageSizes } = collectionConfig.upload; - if (options.req.files && options.req.files.file) { - const fsSafeName = await getSafeFilename(staticDir, options.req.files.file.name); + if (req.files && req.files.file) { + const fsSafeName = await getSafeFilename(staticDir, req.files.file.name); - await options.req.files.file.mv(`${staticDir}/${fsSafeName}`); + await req.files.file.mv(`${staticDir}/${fsSafeName}`); fileData.filename = fsSafeName; - fileData.filesize = options.req.files.file.size; - fileData.mimeType = options.req.files.file.mimetype; + fileData.filesize = req.files.file.size; + fileData.mimeType = req.files.file.mimetype; - if (imageMIMETypes.indexOf(options.req.files.file.mimetype) > -1) { + if (imageMIMETypes.indexOf(req.files.file.mimetype) > -1) { const dimensions = await getImageSize(`${staticDir}/${fsSafeName}`); fileData.width = dimensions.width; fileData.height = dimensions.height; - if (Array.isArray(imageSizes) && options.req.files.file.mimetype !== 'image/svg+xml') { - fileData.sizes = await resizeAndSave(options.config, fsSafeName, fileData.mimeType); + if (Array.isArray(imageSizes) && req.files.file.mimetype !== 'image/svg+xml') { + fileData.sizes = await resizeAndSave(collectionConfig, fsSafeName, fileData.mimeType); } } - options.data = { - ...options.data, + data = { + ...data, ...fileData, }; - } else if (options.data.file === null) { - options.data = { - ...options.data, + } else if (data.file === null) { + data = { + ...data, filename: null, sizes: null, }; @@ -128,7 +143,7 @@ const update = async (args) => { // 6. Perform database operation // ///////////////////////////////////// - Object.assign(doc, options.data); + Object.assign(doc, data); await doc.save(); @@ -138,18 +153,24 @@ const update = async (args) => { // 7. Execute field-level hooks and access // ///////////////////////////////////// - doc = await performFieldOperations(args.config, { - ...options, data: doc, hook: 'afterRead', operationName: 'read', + doc = await performFieldOperations(config, collectionConfig, { + data: doc, + hook: 'afterRead', + operationName: 'read', + req, }); // ///////////////////////////////////// // 8. Execute after collection hook // ///////////////////////////////////// - const { afterUpdate } = args.config.hooks; + const { afterUpdate } = collectionConfig.hooks; if (typeof afterUpdate === 'function') { - doc = await afterUpdate(options, doc) || doc; + doc = (await afterUpdate({ + doc, + req, + })) || doc; } // ///////////////////////////////////// diff --git a/src/collections/requestHandlers/collections.spec.js b/src/collections/requestHandlers/collections.spec.js index 94227a73f..bee5cfb43 100644 --- a/src/collections/requestHandlers/collections.spec.js +++ b/src/collections/requestHandlers/collections.spec.js @@ -206,7 +206,7 @@ describe('Collections - REST', () => { expect(getResponse.status).toBe(200); expect(data.docs[0].description).toBe(desc); - expect(data.docs.length).toBe(1); + expect(data.docs).toHaveLength(1); }); }); @@ -324,7 +324,7 @@ describe('Collections - REST', () => { const data = await getResponse.json(); expect(getResponse.status).toBe(200); - expect(data.docs.length).toEqual(2); + expect(data.docs).toHaveLength(2); expect(data.docs[0].id).toEqual(id1); expect(data.docs[1].id).toEqual(id2); @@ -333,7 +333,7 @@ describe('Collections - REST', () => { const sortedData = await getResponseSorted.json(); expect(getResponse.status).toBe(200); - expect(sortedData.docs.length).toEqual(2); + expect(sortedData.docs).toHaveLength(2); // Opposite order from first request expect(sortedData.docs[0].id).toEqual(id2); expect(sortedData.docs[1].id).toEqual(id1); @@ -361,20 +361,6 @@ describe('Collections - REST', () => { expect(data.doc.description).toEqual('Original-beforeCreateSuffix'); }); - it('beforeRead', async () => { - const response = await fetch(`${url}/api/hooks`, { - headers: { - Authorization: `JWT ${token}`, - 'Content-Type': 'application/json', - hook: 'beforeRead', // Used by hook - }, - method: 'get', - }); - const data = await response.json(); - expect(response.status).toBe(200); - expect(data.limit).toEqual(1); // Set in our beforeRead hook - }); - it('beforeUpdate', async () => { const createResponse = await fetch(`${url}/api/hooks`, { body: JSON.stringify({ diff --git a/src/collections/requestHandlers/create.js b/src/collections/requestHandlers/create.js index 5d3599cdf..7b967beec 100644 --- a/src/collections/requestHandlers/create.js +++ b/src/collections/requestHandlers/create.js @@ -2,12 +2,12 @@ const httpStatus = require('http-status'); const formatSuccessResponse = require('../../express/responses/formatSuccess'); const { create } = require('../operations'); -const createHandler = async (req, res, next) => { +const createHandler = (config) => async (req, res, next) => { try { const doc = await create({ req, - Model: req.collection.Model, - config: req.collection.config, + collection: req.collection, + config, data: req.body, }); diff --git a/src/collections/requestHandlers/delete.js b/src/collections/requestHandlers/delete.js index 3fc93522e..9a0dd2f28 100644 --- a/src/collections/requestHandlers/delete.js +++ b/src/collections/requestHandlers/delete.js @@ -2,12 +2,12 @@ const httpStatus = require('http-status'); const { NotFound } = require('../../errors'); const { deleteQuery } = require('../operations'); -const deleteHandler = async (req, res, next) => { +const deleteHandler = (config) => async (req, res, next) => { try { const doc = await deleteQuery({ req, - Model: req.collection.Model, - config: req.collection.config, + collection: req.collection, + config, id: req.params.id, }); diff --git a/src/collections/requestHandlers/find.js b/src/collections/requestHandlers/find.js index a0a178c51..904a44c90 100644 --- a/src/collections/requestHandlers/find.js +++ b/src/collections/requestHandlers/find.js @@ -1,12 +1,12 @@ const httpStatus = require('http-status'); const { find } = require('../operations'); -const findHandler = async (req, res, next) => { +const findHandler = (config) => async (req, res, next) => { try { const options = { req, - Model: req.collection.Model, - config: req.collection.config, + collection: req.collection, + config, where: req.query.where, page: req.query.page, limit: req.query.limit, diff --git a/src/collections/requestHandlers/findByID.js b/src/collections/requestHandlers/findByID.js index 3b6894c86..35b9eaeba 100644 --- a/src/collections/requestHandlers/findByID.js +++ b/src/collections/requestHandlers/findByID.js @@ -1,10 +1,10 @@ const { findByID } = require('../operations'); -const findByIDHandler = async (req, res, next) => { +const findByIDHandler = (config) => async (req, res, next) => { const options = { req, - Model: req.collection.Model, - config: req.collection.config, + collection: req.collection, + config, id: req.params.id, depth: req.query.depth, }; diff --git a/src/collections/requestHandlers/update.js b/src/collections/requestHandlers/update.js index c0bcd1b84..903c7c9ff 100644 --- a/src/collections/requestHandlers/update.js +++ b/src/collections/requestHandlers/update.js @@ -2,12 +2,12 @@ const httpStatus = require('http-status'); const formatSuccessResponse = require('../../express/responses/formatSuccess'); const { update } = require('../operations'); -const updateHandler = async (req, res, next) => { +const updateHandler = (config) => async (req, res, next) => { try { const doc = await update({ req, - Model: req.collection.Model, - config: req.collection.config, + collection: req.collection, + config, id: req.params.id, data: req.body, }); diff --git a/src/collections/routes.js b/src/collections/routes.js index df72b4859..7a721410a 100644 --- a/src/collections/routes.js +++ b/src/collections/routes.js @@ -9,17 +9,17 @@ const { const router = express.Router(); -const registerRoutes = ({ Model, config }) => { - router.all(`/${config.slug}*`, bindCollectionMiddleware({ Model, config })); +const registerRoutes = (collection, config) => { + router.all(`/${collection.config.slug}*`, bindCollectionMiddleware(collection)); - router.route(`/${config.slug}`) - .get(find) - .post(create); + router.route(`/${collection.config.slug}`) + .get(find(config)) + .post(create(config)); - router.route(`/${config.slug}/:id`) - .get(findByID) - .put(update) - .delete(deleteHandler); + router.route(`/${collection.config.slug}/:id`) + .get(findByID(config)) + .put(update(config)) + .delete(deleteHandler(config)); return router; }; diff --git a/src/fields/performFieldOperations.js b/src/fields/performFieldOperations.js index 279461553..e853e7114 100644 --- a/src/fields/performFieldOperations.js +++ b/src/fields/performFieldOperations.js @@ -1,18 +1,20 @@ const { ValidationError } = require('../errors'); -module.exports = async (config, operation) => { +module.exports = async (config, entityConfig, operation) => { const { data: fullData, originalDoc: fullOriginalDoc, operationName, hook, + req, } = operation; // Maintain a top-level list of promises // so that all async field access / validations / hooks // can run in parallel const validationPromises = []; - const policyPromises = []; + const accessPromises = []; + const relationshipAccessPromises = []; const hookPromises = []; const errors = []; @@ -29,11 +31,21 @@ module.exports = async (config, operation) => { } }; - const createPolicyPromise = async (data, originalDoc, field) => { + const createRelationshipAccessPromise = async (data, field, access) => { + const resultingData = data; + + const result = await access({ req }); + + if (result === false) { + delete resultingData[field.name]; + } + }; + + const createAccessPromise = async (data, originalDoc, field) => { const resultingData = data; if (field.access && field.access[operationName]) { - const result = await field.access[operationName](operation); + const result = await field.access[operationName]({ req }); if (!result && operationName === 'update' && originalDoc[field.name] !== undefined) { resultingData[field.name] = originalDoc[field.name]; @@ -41,6 +53,18 @@ module.exports = async (config, operation) => { delete resultingData[field.name]; } } + + if (field.type === 'relationship' && operationName === 'read') { + const relatedCollections = Array.isArray(field.relationTo) ? field.relationTo : [field.relationTo]; + + relatedCollections.forEach((slug) => { + const collection = config.collections.find((coll) => coll.slug === slug); + + if (collection && collection.access && collection.access.read) { + relationshipAccessPromises.push(createRelationshipAccessPromise(data, field, collection.access.read)); + } + }); + } }; const createHookPromise = async (data, field) => { @@ -66,7 +90,7 @@ module.exports = async (config, operation) => { if (data[field.name] === 'false') dataCopy[field.name] = false; } - policyPromises.push(createPolicyPromise(data, originalDoc, field)); + accessPromises.push(createAccessPromise(data, originalDoc, field)); hookPromises.push(createHookPromise(data, field)); if (field.fields) { @@ -101,14 +125,14 @@ module.exports = async (config, operation) => { // Entry point for field validation // ////////////////////////////////////////// - traverseFields(config.fields, fullData, fullOriginalDoc, ''); + traverseFields(entityConfig.fields, fullData, fullOriginalDoc, ''); await Promise.all(validationPromises); if (errors.length > 0) { throw new ValidationError(errors); } - await Promise.all(policyPromises); + await Promise.all(accessPromises); await Promise.all(hookPromises); return fullData; diff --git a/src/globals/graphql/init.js b/src/globals/graphql/init.js index b56a86c1c..6e51dd5d4 100644 --- a/src/globals/graphql/init.js +++ b/src/globals/graphql/init.js @@ -36,7 +36,7 @@ function registerGlobals() { locale: { type: this.types.localeInputType }, fallbackLocale: { type: this.types.fallbackLocaleInputType }, }, - resolve: findOne(this.globals.Model, global), + resolve: findOne(this.config, this.globals.Model, global), }; this.Mutation.fields[`update${formattedLabel}`] = { @@ -44,7 +44,7 @@ function registerGlobals() { args: { data: { type: global.graphQL.mutationInputType }, }, - resolve: update(this.globals.Model, global), + resolve: update(this.config, this.globals.Model, global), }; }); } diff --git a/src/globals/graphql/resolvers/findOne.js b/src/globals/graphql/resolvers/findOne.js index 852d532e9..9099c55f8 100644 --- a/src/globals/graphql/resolvers/findOne.js +++ b/src/globals/graphql/resolvers/findOne.js @@ -1,15 +1,16 @@ /* eslint-disable no-param-reassign */ const { findOne } = require('../../operations'); -const findOneResolver = (Model, config) => async (_, args, context) => { +const findOneResolver = (config, Model, globalConfig) => async (_, args, context) => { if (args.locale) context.req.locale = args.locale; if (args.fallbackLocale) context.req.fallbackLocale = args.fallbackLocale; - const { slug } = config; + const { slug } = globalConfig; const options = { Model, config, + globalConfig, slug, depth: 0, req: context.req, diff --git a/src/globals/graphql/resolvers/update.js b/src/globals/graphql/resolvers/update.js index 4fe99ee38..00041c1a6 100644 --- a/src/globals/graphql/resolvers/update.js +++ b/src/globals/graphql/resolvers/update.js @@ -1,7 +1,7 @@ /* eslint-disable no-param-reassign */ const { update } = require('../../operations'); -const updateResolver = (Model, config) => async (_, args, context) => { +const updateResolver = (config, Model, globalConfig) => async (_, args, context) => { if (args.locale) context.req.locale = args.locale; if (args.fallbackLocale) context.req.fallbackLocale = args.fallbackLocale; @@ -9,6 +9,7 @@ const updateResolver = (Model, config) => async (_, args, context) => { const options = { config, + globalConfig, Model, data: args.data, slug, diff --git a/src/globals/init.js b/src/globals/init.js index a1bd49218..24333a386 100644 --- a/src/globals/init.js +++ b/src/globals/init.js @@ -8,7 +8,7 @@ function initGlobals() { config: this.config.globals, }; - this.router.use(routes(this.config.globals, this.globals.Model)); + this.router.use(routes(this.config, this.globals.Model)); } } diff --git a/src/globals/operations/findOne.js b/src/globals/operations/findOne.js index f11b85fdd..d02153cd0 100644 --- a/src/globals/operations/findOne.js +++ b/src/globals/operations/findOne.js @@ -1,39 +1,40 @@ -const executeStatic = require('../../auth/executeAccess'); +const executeAccess = require('../../auth/executeAccess'); const performFieldOperations = require('../../fields/performFieldOperations'); const findOne = async (args) => { - // ///////////////////////////////////// - // 1. Retrieve and execute access - // ///////////////////////////////////// - - await executeStatic(args, args.config.access.read); - - let options = { ...args }; - - // ///////////////////////////////////// - // 2. Execute before collection hook - // ///////////////////////////////////// - - const { beforeRead } = args.config.hooks; - - if (typeof beforeRead === 'function') { - options = await beforeRead(options); - } - - // ///////////////////////////////////// - // 3. Perform database operation - // ///////////////////////////////////// - const { - depth, + config, + globalConfig, Model, - slug, + req, req: { payloadAPI, locale, fallbackLocale, }, - } = options; + slug, + depth, + } = args; + + // ///////////////////////////////////// + // 1. Retrieve and execute access + // ///////////////////////////////////// + + await executeAccess({ req }, globalConfig.access.read); + + // ///////////////////////////////////// + // 2. Execute before collection hook + // ///////////////////////////////////// + + const { beforeRead } = globalConfig.hooks; + + if (typeof beforeRead === 'function') { + await beforeRead({ req }); + } + + // ///////////////////////////////////// + // 3. Perform database operation + // ///////////////////////////////////// const queryOptionsToExecute = { options: {}, @@ -53,7 +54,7 @@ const findOne = async (args) => { } let result = await Model.findOne({ globalType: slug }); - let data = {}; + let doc = {}; if (!result) { result = {}; @@ -62,7 +63,7 @@ const findOne = async (args) => { result.setLocale(locale, fallbackLocale); } - data = result.toJSON({ virtuals: true }); + doc = result.toJSON({ virtuals: true }); } @@ -70,25 +71,31 @@ const findOne = async (args) => { // 4. Execute field-level hooks and access // ///////////////////////////////////// - result = performFieldOperations(args.config, { - ...options, data, hook: 'afterRead', operationName: 'read', + doc = performFieldOperations(config, globalConfig, { + doc, + hook: 'afterRead', + operationName: 'read', + req, }); // ///////////////////////////////////// // 5. Execute after collection hook // ///////////////////////////////////// - const { afterRead } = args.config.hooks; + const { afterRead } = globalConfig.hooks; if (typeof afterRead === 'function') { - data = await afterRead(options, result, data) || data; + doc = await afterRead({ + req, + doc, + }) || doc; } // ///////////////////////////////////// // 6. Return results // ///////////////////////////////////// - return data; + return doc; }; module.exports = findOne; diff --git a/src/globals/operations/update.js b/src/globals/operations/update.js index 9c6294a15..c0e7a6a8e 100644 --- a/src/globals/operations/update.js +++ b/src/globals/operations/update.js @@ -1,29 +1,30 @@ const deepmerge = require('deepmerge'); const overwriteMerge = require('../../utilities/overwriteMerge'); -const executeStatic = require('../../auth/executeAccess'); +const executeAccess = require('../../auth/executeAccess'); const performFieldOperations = require('../../fields/performFieldOperations'); const update = async (args) => { - // ///////////////////////////////////// - // 1. Retrieve and execute access - // ///////////////////////////////////// - - await executeStatic(args, args.config.access.update); - - let options = { ...args }; - - // ///////////////////////////////////// - // 2. Retrieve document - // ///////////////////////////////////// - const { + config, + globalConfig, Model, slug, + req, req: { locale, fallbackLocale, }, - } = options; + } = args; + + // ///////////////////////////////////// + // 1. Retrieve and execute access + // ///////////////////////////////////// + + await executeAccess(args, globalConfig.access.update); + + // ///////////////////////////////////// + // 2. Retrieve document + // ///////////////////////////////////// let global = await Model.findOne({ globalType: slug }); @@ -41,29 +42,40 @@ const update = async (args) => { // 3. Execute before global hook // ///////////////////////////////////// - const { beforeUpdate } = args.config.hooks; + let { data } = args; + + const { beforeUpdate } = globalConfig.hooks; if (typeof beforeUpdate === 'function') { - options = await beforeUpdate(options); + data = await beforeUpdate({ + data, + req, + originalDoc: global, + }) || data; } // ///////////////////////////////////// // 4. Merge updates into existing data // ///////////////////////////////////// - options.data = deepmerge(globalJSON, options.data, { arrayMerge: overwriteMerge }); + data = deepmerge(globalJSON, data, { arrayMerge: overwriteMerge }); // ///////////////////////////////////// // 5. Execute field-level hooks, access, and validation // ///////////////////////////////////// - options.data = await performFieldOperations(args.config, { ...options, hook: 'beforeUpdate', operationName: 'update' }); + data = await performFieldOperations(config, globalConfig, { + data, + req, + hook: 'beforeUpdate', + operationName: 'update', + }); // ///////////////////////////////////// // 6. Perform database operation // ///////////////////////////////////// - Object.assign(global, options.data); + Object.assign(global, data); await global.save(); @@ -73,8 +85,11 @@ const update = async (args) => { // 7. Execute field-level hooks and access // ///////////////////////////////////// - global = await performFieldOperations(args.config, { - ...options, data: global, hook: 'afterRead', operationName: 'read', + global = await performFieldOperations(config, globalConfig, { + data: global, + hook: 'afterRead', + operationName: 'read', + req, }); // ///////////////////////////////////// @@ -84,7 +99,10 @@ const update = async (args) => { const { afterUpdate } = args.config.hooks; if (typeof afterUpdate === 'function') { - global = await afterUpdate(options, global); + global = await afterUpdate({ + doc: global, + req, + }) || global; } // ///////////////////////////////////// diff --git a/src/globals/requestHandlers/findOne.js b/src/globals/requestHandlers/findOne.js index 2b6fb6ee1..c72e087b2 100644 --- a/src/globals/requestHandlers/findOne.js +++ b/src/globals/requestHandlers/findOne.js @@ -1,7 +1,7 @@ const httpStatus = require('http-status'); const { findOne } = require('../operations'); -const findOneHandler = (Model, config) => async (req, res, next) => { +const findOneHandler = (config, Model, globalConfig) => async (req, res, next) => { try { const { slug } = config; @@ -9,6 +9,7 @@ const findOneHandler = (Model, config) => async (req, res, next) => { req, Model, config, + globalConfig, slug, depth: req.query.depth, }); diff --git a/src/globals/requestHandlers/update.js b/src/globals/requestHandlers/update.js index f40b455a7..ab1eb0986 100644 --- a/src/globals/requestHandlers/update.js +++ b/src/globals/requestHandlers/update.js @@ -1,7 +1,7 @@ const httpStatus = require('http-status'); const { update } = require('../operations'); -const updateHandler = (Model, config) => async (req, res, next) => { +const updateHandler = (config, Model, globalConfig) => async (req, res, next) => { try { const { slug } = config; @@ -9,6 +9,7 @@ const updateHandler = (Model, config) => async (req, res, next) => { req, Model, config, + globalConfig, slug, depth: req.query.depth, data: req.body, diff --git a/src/globals/routes.js b/src/globals/routes.js index ec2016bac..0ab285d4d 100644 --- a/src/globals/routes.js +++ b/src/globals/routes.js @@ -5,12 +5,12 @@ const { update, findOne } = requestHandlers; const router = express.Router(); -const registerGlobals = (globalConfigs, Globals) => { - globalConfigs.forEach((global) => { +const registerGlobals = (config, Globals) => { + config.globals.forEach((global) => { router .route(`/globals/${global.slug}`) - .get(findOne(Globals, global)) - .post(update(Globals, global)); + .get(findOne(config, Globals, global)) + .post(update(config, Globals, global)); }); return router; diff --git a/src/mongoose/buildSchema.js b/src/mongoose/buildSchema.js index 7a58cc0c5..c2dde379a 100644 --- a/src/mongoose/buildSchema.js +++ b/src/mongoose/buildSchema.js @@ -3,13 +3,13 @@ const { Schema } = require('mongoose'); const { MissingSelectOptions } = require('../errors'); const formatBaseSchema = (field) => { - const createPolicy = field.access && field.access.create; + const createAccess = field.access && field.access.create; return { hide: field.hidden, localized: field.localized || false, unique: field.unique || false, - required: (field.required && !field.localized && !field.condition && !createPolicy) || false, + required: (field.required && !field.localized && !field.condition && !createAccess) || false, default: field.defaultValue || undefined, }; }; From 656d96e04d486398e75def2d6ab59d07ce6acc9e Mon Sep 17 00:00:00 2001 From: James Date: Fri, 10 Jul 2020 02:08:37 -0400 Subject: [PATCH 045/125] closes mobile nav on item click, increases nav item size --- src/client/components/elements/Nav/index.js | 13 +++++++++---- src/client/components/elements/Nav/index.scss | 6 ++++++ 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/client/components/elements/Nav/index.js b/src/client/components/elements/Nav/index.js index 6bb43a909..16f9976ea 100644 --- a/src/client/components/elements/Nav/index.js +++ b/src/client/components/elements/Nav/index.js @@ -1,5 +1,5 @@ -import React, { useState } from 'react'; -import { NavLink, Link } from 'react-router-dom'; +import React, { useState, useEffect } from 'react'; +import { NavLink, Link, useHistory } from 'react-router-dom'; import config from 'payload/config'; import { useUser } from '../../data/User'; import Chevron from '../../icons/Chevron'; @@ -25,12 +25,17 @@ const { const Nav = () => { const { permissions } = useUser(); const [menuActive, setMenuActive] = useState(false); + const history = useHistory(); const classes = [ baseClass, menuActive && `${baseClass}--menu-active`, ].filter(Boolean).join(' '); + useEffect(() => history.listen(() => { + setMenuActive(false); + }), []); + return (
    { - const field = fields.find(fieldToCheck => fieldToCheck.name === col); + const field = fields.find((fieldToCheck) => fieldToCheck.name === col); if (field) { return [ @@ -93,17 +93,15 @@ const DefaultList = (props) => { disable={field.disableSort || undefined} /> ), - renderCell: (rowData, cellData) => { - return ( - - ); - }, + renderCell: (rowData, cellData) => ( + + ), }, }, ]; @@ -117,10 +115,10 @@ const DefaultList = (props) => { history.push(`${admin}/collections/${slug}/${doc.id}`)} + onCardClick={(doc) => history.push(`${admin}/collections/${slug}/${doc.id}`)} /> )} - + )} {data.docs && data.docs.length === 0 && (
    @@ -181,7 +179,9 @@ DefaultList.propTypes = { plural: PropTypes.string, }), slug: PropTypes.string, - useAsTitle: PropTypes.string, + admin: PropTypes.shape({ + useAsTitle: PropTypes.string, + }), fields: PropTypes.arrayOf(PropTypes.shape), timestamps: PropTypes.bool, }).isRequired, diff --git a/src/client/components/views/collections/List/index.js b/src/client/components/views/collections/List/index.js index 3de6ef99c..52a79d042 100644 --- a/src/client/components/views/collections/List/index.js +++ b/src/client/components/views/collections/List/index.js @@ -50,7 +50,6 @@ ListView.propTypes = { plural: PropTypes.string, }), slug: PropTypes.string, - useAsTitle: PropTypes.string, fields: PropTypes.arrayOf(PropTypes.shape), timestamps: PropTypes.bool, }).isRequired, diff --git a/src/collections/sanitize.js b/src/collections/sanitize.js index ce225a1a5..6d2f36bc8 100644 --- a/src/collections/sanitize.js +++ b/src/collections/sanitize.js @@ -75,6 +75,7 @@ const sanitizeCollection = (collections, collection) => { if (!sanitized.hooks) sanitized.hooks = {}; if (!sanitized.access) sanitized.access = {}; + if (!sanitized.admin) sanitized.admin = {}; if (!sanitized.hooks.beforeCreate) sanitized.hooks.beforeCreate = []; if (!sanitized.hooks.afterCreate) sanitized.hooks.afterCreate = []; @@ -89,7 +90,7 @@ const sanitizeCollection = (collections, collection) => { if (sanitized.upload) { if (!sanitized.upload.staticDir) sanitized.upload.staticDir = sanitized.slug; if (!sanitized.upload.staticURL) sanitized.upload.staticURL = sanitized.slug; - if (!sanitized.useAsTitle) sanitized.useAsTitle = 'filename'; + if (!sanitized.admin.useAsTitle) sanitized.admin.useAsTitle = 'filename'; } // ///////////////////////////////// @@ -104,24 +105,24 @@ const sanitizeCollection = (collections, collection) => { type: 'text', required: true, unique: true, - readOnly: true, admin: { disabled: true, + readOnly: true, }, }, { name: 'mimeType', label: 'MIME Type', type: 'text', - readOnly: true, admin: { + readOnly: true, disabled: true, }, }, { name: 'filesize', label: 'File Size', type: 'number', - readOnly: true, admin: { + readOnly: true, disabled: true, }, }, @@ -133,16 +134,16 @@ const sanitizeCollection = (collections, collection) => { name: 'width', label: 'Width', type: 'number', - readOnly: true, admin: { + readOnly: true, disabled: true, }, }, { name: 'height', label: 'Height', type: 'number', - readOnly: true, admin: { + readOnly: true, disabled: true, }, }, @@ -165,40 +166,40 @@ const sanitizeCollection = (collections, collection) => { name: 'width', label: 'Width', type: 'number', - readOnly: true, admin: { + readOnly: true, disabled: true, }, }, { name: 'height', label: 'Height', type: 'number', - readOnly: true, admin: { + readOnly: true, disabled: true, }, }, { name: 'mimeType', label: 'MIME Type', type: 'text', - readOnly: true, admin: { + readOnly: true, disabled: true, }, }, { name: 'filesize', label: 'File Size', type: 'number', - readOnly: true, admin: { + readOnly: true, disabled: true, }, }, { name: 'filename', label: 'File Name', type: 'text', - readOnly: true, admin: { + readOnly: true, disabled: true, }, }, diff --git a/src/fields/performFieldOperations.js b/src/fields/performFieldOperations.js index 90c72102b..e37cffd15 100644 --- a/src/fields/performFieldOperations.js +++ b/src/fields/performFieldOperations.js @@ -19,7 +19,8 @@ module.exports = async (config, entityConfig, operation) => { const errors = []; const createValidationPromise = async (data, field, path) => { - const shouldValidate = field.validate && !field.condition; + const hasCondition = field.admin && field.admin.condition; + const shouldValidate = field.validate && !hasCondition; const dataToValidate = data || field.defaultValue; const result = shouldValidate ? await field.validate(dataToValidate, field) : true; diff --git a/src/globals/sanitize.js b/src/globals/sanitize.js index 951ee7625..6990a4333 100644 --- a/src/globals/sanitize.js +++ b/src/globals/sanitize.js @@ -21,6 +21,7 @@ const sanitizeGlobals = (globals) => { if (!sanitizedGlobal.hooks) sanitizedGlobal.hooks = {}; if (!sanitizedGlobal.access) sanitizedGlobal.access = {}; + if (!sanitizedGlobal.admin) sanitizedGlobal.admin = {}; if (!sanitizedGlobal.hooks.beforeUpdate) sanitizedGlobal.hooks.beforeUpdate = []; if (!sanitizedGlobal.hooks.afterUpdate) sanitizedGlobal.hooks.afterUpdate = []; diff --git a/src/mongoose/buildSchema.js b/src/mongoose/buildSchema.js index c2dde379a..f9e54e8b6 100644 --- a/src/mongoose/buildSchema.js +++ b/src/mongoose/buildSchema.js @@ -5,11 +5,13 @@ const { MissingSelectOptions } = require('../errors'); const formatBaseSchema = (field) => { const createAccess = field.access && field.access.create; + const condition = field.admin && field.admin.condition; + return { hide: field.hidden, localized: field.localized || false, unique: field.unique || false, - required: (field.required && !field.localized && !field.condition && !createAccess) || false, + required: (field.required && !field.localized && !condition && !createAccess) || false, default: field.defaultValue || undefined, }; }; diff --git a/src/webpack/getWebpackProdConfig.js b/src/webpack/getWebpackProdConfig.js index 19bac79fb..ffcedd54c 100644 --- a/src/webpack/getWebpackProdConfig.js +++ b/src/webpack/getWebpackProdConfig.js @@ -1,7 +1,7 @@ const HtmlWebpackPlugin = require('html-webpack-plugin'); const Dotenv = require('dotenv-webpack'); // const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer'); -const { IgnorePlugin } = require('webpack'); +const webpack = require('webpack'); const path = require('path'); const getStyleLoaders = require('./getStyleLoaders'); @@ -124,20 +124,21 @@ module.exports = (config) => { new Dotenv({ silent: true, }), + new webpack.optimize.UglifyJSPlugin(), ]; if (config.webpackIgnorePlugin instanceof RegExp) { - plugins.push(new IgnorePlugin(config.webpackIgnorePlugin)); + plugins.push(new webpack.IgnorePlugin(config.webpackIgnorePlugin)); } else if (typeof config.webpackIgnorePlugin === 'string') { - plugins.push(new IgnorePlugin(new RegExp(`^${config.webpackIgnorePlugin}$`, 'is'))); + plugins.push(new webpack.IgnorePlugin(new RegExp(`^${config.webpackIgnorePlugin}$`, 'is'))); } if (Array.isArray(config.webpackIgnorePlugin)) { config.webpackIgnorePlugin.forEach((ignorePath) => { if (ignorePath instanceof RegExp) { - plugins.push(new IgnorePlugin(ignorePath)); + plugins.push(new webpack.IgnorePlugin(ignorePath)); } else if (typeof ignorePath === 'string') { - plugins.push(new IgnorePlugin(new RegExp(`^${ignorePath}$`, 'is'))); + plugins.push(new webpack.IgnorePlugin(new RegExp(`^${ignorePath}$`, 'is'))); } }); } From eb4c9727605121424da2808b040eca8b239af4be Mon Sep 17 00:00:00 2001 From: James Date: Sat, 18 Jul 2020 16:12:05 -0400 Subject: [PATCH 084/125] 0.0.13 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 19c2052b6..9e52facc8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@payloadcms/payload", - "version": "0.0.12", + "version": "0.0.13", "description": "CMS and Application Framework", "license": "ISC", "author": "Payload CMS LLC", From 7d18c92ec2a389ac852be08f430e59f022ab023a Mon Sep 17 00:00:00 2001 From: James Date: Sat, 18 Jul 2020 16:51:09 -0400 Subject: [PATCH 085/125] sanitizes config before build --- package.json | 2 +- src/bin/build.js | 4 +- src/client/components/customComponents.js | 2 +- src/utilities/sanitizeConfig.js | 1 + src/webpack/getWebpackProdConfig.js | 5 +- yarn.lock | 108 +++++----------------- 6 files changed, 35 insertions(+), 87 deletions(-) diff --git a/package.json b/package.json index 9e52facc8..a6bd7a123 100644 --- a/package.json +++ b/package.json @@ -106,7 +106,7 @@ "slate-react": "^0.58.3", "style-loader": "^0.21.0", "styled-components": "^5.1.1", - "uglifyjs-webpack-plugin": "^1.3.0", + "uglifyjs-webpack-plugin": "^2.2.0", "url-loader": "^1.0.1", "uuid": "^8.1.0", "val-loader": "^2.1.0", diff --git a/src/bin/build.js b/src/bin/build.js index 2f2f66a46..c850e5701 100755 --- a/src/bin/build.js +++ b/src/bin/build.js @@ -4,12 +4,14 @@ const webpack = require('webpack'); const getWebpackProdConfig = require('../webpack/getWebpackProdConfig'); const findConfig = require('../utilities/findConfig'); +const sanitizeConfig = require('../utilities/sanitizeConfig'); module.exports = () => { const configPath = findConfig(); try { - const config = require(configPath); + const unsanitizedConfig = require(configPath); + const config = sanitizeConfig(unsanitizedConfig); const webpackProdConfig = getWebpackProdConfig({ ...config, diff --git a/src/client/components/customComponents.js b/src/client/components/customComponents.js index a02ebf0a8..3dcedf273 100644 --- a/src/client/components/customComponents.js +++ b/src/client/components/customComponents.js @@ -24,7 +24,7 @@ function recursivelyAddFieldComponents(fields) { }; } - if (field.components || field.fields) { + if (field.admin.components || field.fields) { const fieldComponents = { ...(field.admin.components || {}), }; diff --git a/src/utilities/sanitizeConfig.js b/src/utilities/sanitizeConfig.js index a48a50f03..daf168093 100644 --- a/src/utilities/sanitizeConfig.js +++ b/src/utilities/sanitizeConfig.js @@ -38,6 +38,7 @@ const sanitizeConfig = (config) => { sanitizedConfig.components = { ...(config.components || {}) }; sanitizedConfig.hooks = { ...(config.hooks || {}) }; + sanitizedConfig.admin = { ...(config.admin || {}) }; return sanitizedConfig; }; diff --git a/src/webpack/getWebpackProdConfig.js b/src/webpack/getWebpackProdConfig.js index ffcedd54c..67e512bc6 100644 --- a/src/webpack/getWebpackProdConfig.js +++ b/src/webpack/getWebpackProdConfig.js @@ -2,6 +2,7 @@ const HtmlWebpackPlugin = require('html-webpack-plugin'); const Dotenv = require('dotenv-webpack'); // const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer'); const webpack = require('webpack'); +const UglifyJsPlugin = require('uglifyjs-webpack-plugin'); const path = require('path'); const getStyleLoaders = require('./getStyleLoaders'); @@ -15,6 +16,9 @@ module.exports = (config) => { publicPath: `${config.routes.admin}/`, filename: '[name].[chunkhash].js', }, + optimization: { + minimizer: [new UglifyJsPlugin()], + }, mode: 'production', resolveLoader: { modules: ['node_modules', path.join(__dirname, '../../node_modules')] }, module: { @@ -124,7 +128,6 @@ module.exports = (config) => { new Dotenv({ silent: true, }), - new webpack.optimize.UglifyJSPlugin(), ]; if (config.webpackIgnorePlugin instanceof RegExp) { diff --git a/yarn.lock b/yarn.lock index 73287f32e..f0f336f6b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2353,7 +2353,7 @@ bluebird@3.5.1: resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.1.tgz#d9551f9de98f1fcda1e683d17ee91a0602ee2eb9" integrity sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA== -bluebird@^3.5.1, bluebird@^3.5.5: +bluebird@^3.5.5: version "3.7.2" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== @@ -2602,25 +2602,6 @@ bytes@3.1.0: resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== -cacache@^10.0.4: - version "10.0.4" - resolved "https://registry.yarnpkg.com/cacache/-/cacache-10.0.4.tgz#6452367999eff9d4188aefd9a14e9d7c6a263460" - integrity sha512-Dph0MzuH+rTQzGPNT9fAnrPmMmjKfST6trxJeK7NQuHRaVw24VzPRWTmg9MpcwOVQZO0E1FBICUlFeNaKPIfHA== - dependencies: - bluebird "^3.5.1" - chownr "^1.0.1" - glob "^7.1.2" - graceful-fs "^4.1.11" - lru-cache "^4.1.1" - mississippi "^2.0.0" - mkdirp "^0.5.1" - move-concurrently "^1.0.1" - promise-inflight "^1.0.1" - rimraf "^2.6.2" - ssri "^5.2.4" - unique-filename "^1.1.0" - y18n "^4.0.0" - cacache@^12.0.2: version "12.0.4" resolved "https://registry.yarnpkg.com/cacache/-/cacache-12.0.4.tgz#668bcbd105aeb5f1d92fe25570ec9525c8faa40c" @@ -2834,7 +2815,7 @@ chokidar@^3.4.0: optionalDependencies: fsevents "~2.1.2" -chownr@^1.0.1, chownr@^1.1.1: +chownr@^1.1.1: version "1.1.4" resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== @@ -3048,11 +3029,6 @@ commander@^2.18.0, commander@^2.20.0, commander@^2.20.3: resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== -commander@~2.13.0: - version "2.13.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.13.0.tgz#6964bca67685df7c1f1430c584f07d7597885b9c" - integrity sha512-MVuS359B+YzaWqjCL/c+22gfryv+mCBPHAv3zyVI2GN8EY6IRP8VwtasXn8jyyhvvq84R4ImN1OKRtcbIasjYA== - commander@~2.19.0: version "2.19.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a" @@ -4726,15 +4702,6 @@ find-cache-dir@3.3.1, find-cache-dir@^3.3.1: make-dir "^3.0.2" pkg-dir "^4.1.0" -find-cache-dir@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-1.0.0.tgz#9288e3e9e3cc3748717d39eade17cf71fc30ee6f" - integrity sha1-kojj6ePMN0hxfTnq3hfPcfww7m8= - dependencies: - commondir "^1.0.1" - make-dir "^1.0.0" - pkg-dir "^2.0.0" - find-cache-dir@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-2.1.0.tgz#8d0f94cd13fe43c6c7c261a0d86115ca918c05f7" @@ -6903,7 +6870,7 @@ lowercase-keys@^1.0.0: resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f" integrity sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA== -lru-cache@^4.0.1, lru-cache@^4.1.1: +lru-cache@^4.0.1: version "4.1.5" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd" integrity sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g== @@ -7182,22 +7149,6 @@ minizlib@^2.1.0: minipass "^3.0.0" yallist "^4.0.0" -mississippi@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/mississippi/-/mississippi-2.0.0.tgz#3442a508fafc28500486feea99409676e4ee5a6f" - integrity sha512-zHo8v+otD1J10j/tC+VNoGK9keCuByhKovAvdn74dmxJl9+mWHnx6EMsDN4lgRoMI/eYo2nchAxniIbUPb5onw== - dependencies: - concat-stream "^1.5.0" - duplexify "^3.4.2" - end-of-stream "^1.1.0" - flush-write-stream "^1.0.0" - from2 "^2.1.0" - parallel-transform "^1.1.0" - pump "^2.0.1" - pumpify "^1.3.3" - stream-each "^1.1.0" - through2 "^2.0.0" - mississippi@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/mississippi/-/mississippi-3.0.0.tgz#ea0a3291f97e0b5e8776b363d5f0a12d94c67022" @@ -9027,7 +8978,7 @@ public-encrypt@^4.0.0: randombytes "^2.0.1" safe-buffer "^5.1.2" -pump@^2.0.0, pump@^2.0.1: +pump@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/pump/-/pump-2.0.1.tgz#12399add6e4cf7526d973cbc8b5ce2e2908b3909" integrity sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA== @@ -9745,7 +9696,7 @@ rgba-regex@^1.0.0: resolved "https://registry.yarnpkg.com/rgba-regex/-/rgba-regex-1.0.0.tgz#43374e2e2ca0968b0ef1523460b7d730ff22eeb3" integrity sha1-QzdOLiyglosO8VI0YLfXMP8i7rM= -rimraf@2, rimraf@^2.5.4, rimraf@^2.6.2, rimraf@^2.6.3: +rimraf@2, rimraf@^2.5.4, rimraf@^2.6.3: version "2.7.1" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== @@ -10015,7 +9966,7 @@ send@0.17.1: range-parser "~1.2.1" statuses "~1.5.0" -serialize-javascript@^1.4.0: +serialize-javascript@^1.7.0: version "1.9.1" resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-1.9.1.tgz#cfc200aef77b600c47da9bb8149c943e798c2fdb" integrity sha512-0Vb/54WJ6k5v8sSWN09S0ora+Hnr+cX40r9F170nT+mSkaxltoE/7R3OrIdBSUv1OoiobH1QoWQbCnAO+e8J1A== @@ -10400,13 +10351,6 @@ sshpk@^1.7.0: safer-buffer "^2.0.2" tweetnacl "~0.14.0" -ssri@^5.2.4: - version "5.3.0" - resolved "https://registry.yarnpkg.com/ssri/-/ssri-5.3.0.tgz#ba3872c9c6d33a0704a7d71ff045e5ec48999d06" - integrity sha512-XRSIPqLij52MtgoQavH/x/dU1qVKtWUAAZeOHsR9c2Ddi4XerFy3mc1alf+dLJKl9EUIm/Ht+EowFkTUOA6GAQ== - dependencies: - safe-buffer "^5.1.1" - ssri@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/ssri/-/ssri-6.0.1.tgz#2a3c41b28dd45b62b63676ecb74001265ae9edd8" @@ -11177,14 +11121,6 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= -uglify-es@^3.3.4: - version "3.3.9" - resolved "https://registry.yarnpkg.com/uglify-es/-/uglify-es-3.3.9.tgz#0c1c4f0700bed8dbc124cdb304d2592ca203e677" - integrity sha512-r+MU0rfv4L/0eeW3xZrd16t4NZfK8Ld4SWVglYBb7ez5uXFWHuVRs6xCTrf1yirs9a4j4Y27nn7SRfO6v67XsQ== - dependencies: - commander "~2.13.0" - source-map "~0.6.1" - uglify-js@3.4.x: version "3.4.10" resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.4.10.tgz#9ad9563d8eb3acdfb8d38597d2af1d815f6a755f" @@ -11193,19 +11129,25 @@ uglify-js@3.4.x: commander "~2.19.0" source-map "~0.6.1" -uglifyjs-webpack-plugin@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-1.3.0.tgz#75f548160858163a08643e086d5fefe18a5d67de" - integrity sha512-ovHIch0AMlxjD/97j9AYovZxG5wnHOPkL7T1GKochBADp/Zwc44pEWNqpKl1Loupp1WhFg7SlYmHZRUfdAacgw== +uglify-js@^3.6.0: + version "3.10.0" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.10.0.tgz#397a7e6e31ce820bfd1cb55b804ee140c587a9e7" + integrity sha512-Esj5HG5WAyrLIdYU74Z3JdG2PxdIusvj6IWHMtlyESxc7kcDz7zYlYjpnSokn1UbpV0d/QX9fan7gkCNd/9BQA== + +uglifyjs-webpack-plugin@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-2.2.0.tgz#e75bc80e7f1937f725954c9b4c5a1e967ea9d0d7" + integrity sha512-mHSkufBmBuJ+KHQhv5H0MXijtsoA1lynJt1lXOaotja8/I0pR4L9oGaPIZw+bQBOFittXZg9OC1sXSGO9D9ZYg== dependencies: - cacache "^10.0.4" - find-cache-dir "^1.0.0" - schema-utils "^0.4.5" - serialize-javascript "^1.4.0" + cacache "^12.0.2" + find-cache-dir "^2.1.0" + is-wsl "^1.1.0" + schema-utils "^1.0.0" + serialize-javascript "^1.7.0" source-map "^0.6.1" - uglify-es "^3.3.4" - webpack-sources "^1.1.0" - worker-farm "^1.5.2" + uglify-js "^3.6.0" + webpack-sources "^1.4.0" + worker-farm "^1.7.0" undefsafe@^2.0.2: version "2.0.3" @@ -11257,7 +11199,7 @@ uniqs@^2.0.0: resolved "https://registry.yarnpkg.com/uniqs/-/uniqs-2.0.0.tgz#ffede4b36b25290696e6e165d4a59edb998e6b02" integrity sha1-/+3ks2slKQaW5uFl1KWe25mOawI= -unique-filename@^1.1.0, unique-filename@^1.1.1: +unique-filename@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-1.1.1.tgz#1d69769369ada0583103a1e6ae87681b56573230" integrity sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ== @@ -11716,7 +11658,7 @@ word-wrap@~1.2.3: resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== -worker-farm@^1.5.2, worker-farm@^1.7.0: +worker-farm@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.7.0.tgz#26a94c5391bbca926152002f69b84a4bf772e5a8" integrity sha512-rvw3QTZc8lAxyVrqcSGVm5yP/IJ2UcB3U0graE3LCFoZ0Yn2x4EoVSqJKdB/T5M+FLcRPjz4TDacRf3OCfNUzw== From f04423106ac531dc3ab9eec3e6cb7dc3da7db784 Mon Sep 17 00:00:00 2001 From: James Date: Sat, 18 Jul 2020 16:51:19 -0400 Subject: [PATCH 086/125] 0.0.14 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a6bd7a123..d611c6026 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@payloadcms/payload", - "version": "0.0.13", + "version": "0.0.14", "description": "CMS and Application Framework", "license": "ISC", "author": "Payload CMS LLC", From 809fc7ccd9f5a89c3a13aa6c8b81fce42f173a4a Mon Sep 17 00:00:00 2001 From: James Date: Sat, 18 Jul 2020 16:54:19 -0400 Subject: [PATCH 087/125] removes breaking uglifyJS --- src/webpack/getWebpackProdConfig.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/webpack/getWebpackProdConfig.js b/src/webpack/getWebpackProdConfig.js index 67e512bc6..832190ddc 100644 --- a/src/webpack/getWebpackProdConfig.js +++ b/src/webpack/getWebpackProdConfig.js @@ -2,7 +2,6 @@ const HtmlWebpackPlugin = require('html-webpack-plugin'); const Dotenv = require('dotenv-webpack'); // const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer'); const webpack = require('webpack'); -const UglifyJsPlugin = require('uglifyjs-webpack-plugin'); const path = require('path'); const getStyleLoaders = require('./getStyleLoaders'); @@ -16,9 +15,6 @@ module.exports = (config) => { publicPath: `${config.routes.admin}/`, filename: '[name].[chunkhash].js', }, - optimization: { - minimizer: [new UglifyJsPlugin()], - }, mode: 'production', resolveLoader: { modules: ['node_modules', path.join(__dirname, '../../node_modules')] }, module: { From b6c5c5b92cb4a892a85a5d0086c55139594a696d Mon Sep 17 00:00:00 2001 From: James Date: Sat, 18 Jul 2020 16:55:21 -0400 Subject: [PATCH 088/125] 0.0.15 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d611c6026..093c9a0d2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@payloadcms/payload", - "version": "0.0.14", + "version": "0.0.15", "description": "CMS and Application Framework", "license": "ISC", "author": "Payload CMS LLC", From c104d794385cb6a0354912f8fd56fba2c287ed6a Mon Sep 17 00:00:00 2001 From: Elliot DeNolf Date: Sun, 19 Jul 2020 12:17:28 -0400 Subject: [PATCH 089/125] test(collections): add 'OR' tests --- .../requestHandlers/collections.spec.js | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/src/collections/requestHandlers/collections.spec.js b/src/collections/requestHandlers/collections.spec.js index 50fd284da..03855c390 100644 --- a/src/collections/requestHandlers/collections.spec.js +++ b/src/collections/requestHandlers/collections.spec.js @@ -208,6 +208,87 @@ describe('Collections - REST', () => { expect(data.docs[0].description).toBe(desc); expect(data.docs).toHaveLength(1); }); + + it('should allow querying with OR', async () => { + const title1 = 'Or1'; + const title2 = 'Or2'; + const response = await fetch(`${url}/api/localized-posts`, { + body: JSON.stringify({ + title: title1, + description: 'desc', + priority: 1, + }), + headers: { + Authorization: `JWT ${token}`, + 'Content-Type': 'application/json', + }, + method: 'post', + }); + + const response2 = await fetch(`${url}/api/localized-posts`, { + body: JSON.stringify({ + title: title2, + description: 'desc', + priority: 1, + }), + headers: { + Authorization: `JWT ${token}`, + 'Content-Type': 'application/json', + }, + method: 'post', + }); + + expect(response.status).toBe(201); + expect(response2.status).toBe(201); + + const queryResponse = await fetch(`${url}/api/localized-posts?where[or][0][title][equals]=${title1}&where[or][1][title][equals]=${title2}`); + const data = await queryResponse.json(); + + expect(queryResponse.status).toBe(200); + expect(data.docs).toHaveLength(2); + expect(data.docs).toContainEqual(expect.objectContaining({ title: title1 })); + expect(data.docs).toContainEqual(expect.objectContaining({ title: title2 })); + }); + + it('should allow querying with OR, 1 result', async () => { + const title1 = 'OrNegative1'; + const title2 = 'OrNegative2'; + const response = await fetch(`${url}/api/localized-posts`, { + body: JSON.stringify({ + title: title1, + description: 'desc', + priority: 1, + }), + headers: { + Authorization: `JWT ${token}`, + 'Content-Type': 'application/json', + }, + method: 'post', + }); + + const response2 = await fetch(`${url}/api/localized-posts`, { + body: JSON.stringify({ + title: title2, + description: 'desc', + priority: 1, + }), + headers: { + Authorization: `JWT ${token}`, + 'Content-Type': 'application/json', + }, + method: 'post', + }); + + expect(response.status).toBe(201); + expect(response2.status).toBe(201); + + const queryResponse = await fetch(`${url}/api/localized-posts?where[or][0][title][equals]=${title1}&where[or][1][title][equals]=nonexistent`); + const data = await queryResponse.json(); + + expect(queryResponse.status).toBe(200); + expect(data.docs).toHaveLength(1); + expect(data.docs[0].title).toBe(title1); + }); }); describe('Delete', () => { From 26b5109bca65b6fcbcab030bfa8e138f6a5560e3 Mon Sep 17 00:00:00 2001 From: James Date: Sun, 19 Jul 2020 12:56:00 -0400 Subject: [PATCH 090/125] css fixes to array / blocks, file handling improvements --- .../forms/DraggableSection/index.scss | 13 ++++++---- .../forms/field-types/Array/index.scss | 7 +++-- .../forms/field-types/Blocks/index.scss | 9 +++++++ .../forms/field-types/Upload/index.js | 9 +++++-- src/collections/operations/create.js | 26 +++++++++---------- src/collections/operations/delete.js | 12 ++++++--- src/collections/sanitize.js | 2 +- 7 files changed, 51 insertions(+), 27 deletions(-) diff --git a/src/client/components/forms/DraggableSection/index.scss b/src/client/components/forms/DraggableSection/index.scss index dab535547..625bbcd0d 100644 --- a/src/client/components/forms/DraggableSection/index.scss +++ b/src/client/components/forms/DraggableSection/index.scss @@ -4,7 +4,7 @@ // HELPER MIXINS ////////////////////// -@mixin realtively-position-panels { +@mixin relatively-position-panels { .position-panel { position: relative; right: 0; @@ -117,7 +117,7 @@ } @include mid-break { - @include realtively-position-panels(); + @include relatively-position-panels(); .position-panel__move-forward, .position-panel__move-backward { @@ -135,12 +135,15 @@ @include absolutely-position-panels(); @include mid-break { - @include realtively-position-panels(); + @include relatively-position-panels(); } } -.field-type.array .field-type.array { - @include realtively-position-panels(); +.field-type.blocks .field-type.array, +.field-type.array .field-type.array, +.field-type.array .field-type.blocks, +.field-type.blocks .field-type.blocks { + @include relatively-position-panels(); } // remove padding above array rows to level diff --git a/src/client/components/forms/field-types/Array/index.scss b/src/client/components/forms/field-types/Array/index.scss index f5c4eceb5..59a82bc0f 100644 --- a/src/client/components/forms/field-types/Array/index.scss +++ b/src/client/components/forms/field-types/Array/index.scss @@ -25,13 +25,16 @@ width: 100%; } } +} +.field-type.array, +.field-type.blocks { .field-type.array { - .field-type.repeater__add-button-wrap { + .field-type.array__add-button-wrap { margin-left: base(2.65); } - .field-type.repeater__header { + .field-type.array__header { display: none; } } diff --git a/src/client/components/forms/field-types/Blocks/index.scss b/src/client/components/forms/field-types/Blocks/index.scss index b1f94c9fa..daff6276d 100644 --- a/src/client/components/forms/field-types/Blocks/index.scss +++ b/src/client/components/forms/field-types/Blocks/index.scss @@ -24,3 +24,12 @@ } } } + +.field-type.array, +.field-type.blocks { + .field-type.blocks { + .field-type.blocks__add-button-wrap { + margin-left: base(2.65); + } + } +} diff --git a/src/client/components/forms/field-types/Upload/index.js b/src/client/components/forms/field-types/Upload/index.js index 304489c6f..ddaf40112 100644 --- a/src/client/components/forms/field-types/Upload/index.js +++ b/src/client/components/forms/field-types/Upload/index.js @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useCallback } from 'react'; import PropTypes from 'prop-types'; import { useModal } from '@faceless-ui/modal'; import config from '../../../../config'; @@ -47,12 +47,17 @@ const Upload = (props) => { const dataToInitialize = (typeof initialData === 'object' && initialData.id) ? initialData.id : initialData; + const memoizedValidate = useCallback((value) => { + const validationResult = validate(value, { required }); + return validationResult; + }, [validate, required]); + const fieldType = useFieldType({ path, required, initialData: dataToInitialize, defaultValue, - validate, + validate: memoizedValidate, }); const { diff --git a/src/collections/operations/create.js b/src/collections/operations/create.js index 8c5ef6f72..71c68431d 100644 --- a/src/collections/operations/create.js +++ b/src/collections/operations/create.js @@ -46,18 +46,7 @@ const create = async (args) => { }, Promise.resolve()); // ///////////////////////////////////// - // 3. Execute field-level access, hooks, and validation - // ///////////////////////////////////// - - data = await performFieldOperations(config, collectionConfig, { - data, - hook: 'beforeCreate', - operationName: 'create', - req, - }); - - // ///////////////////////////////////// - // 4. Upload and resize any files that may be present + // 3. Upload and resize any files that may be present // ///////////////////////////////////// if (collectionConfig.upload) { @@ -69,7 +58,7 @@ const create = async (args) => { throw new MissingFile(); } - await mkdirp(staticDir); + mkdirp.sync(staticDir); const fsSafeName = await getSafeFilename(staticDir, req.files.file.name); @@ -95,6 +84,17 @@ const create = async (args) => { }; } + // ///////////////////////////////////// + // 4. Execute field-level access, hooks, and validation + // ///////////////////////////////////// + + data = await performFieldOperations(config, collectionConfig, { + data, + hook: 'beforeCreate', + operationName: 'create', + req, + }); + // ///////////////////////////////////// // 5. Perform database operation // ///////////////////////////////////// diff --git a/src/collections/operations/delete.js b/src/collections/operations/delete.js index 7d762d19a..386f0692a 100644 --- a/src/collections/operations/delete.js +++ b/src/collections/operations/delete.js @@ -60,14 +60,18 @@ const deleteQuery = async (args) => { if (collectionConfig.upload) { const { staticDir } = collectionConfig.upload; - fs.unlink(`${staticDir}/${resultToDelete.filename}`, () => { - throw new ErrorDeletingFile(); + fs.unlink(`${staticDir}/${resultToDelete.filename}`, (err) => { + if (err) { + throw new ErrorDeletingFile(); + } }); if (resultToDelete.sizes) { Object.values(resultToDelete.sizes).forEach((size) => { - fs.unlink(`${staticDir}/${size.filename}`, () => { - throw new ErrorDeletingFile(); + fs.unlink(`${staticDir}/${size.filename}`, (err) => { + if (err) { + throw new ErrorDeletingFile(); + } }); }); } diff --git a/src/collections/sanitize.js b/src/collections/sanitize.js index 6d2f36bc8..a99b72e71 100644 --- a/src/collections/sanitize.js +++ b/src/collections/sanitize.js @@ -89,7 +89,7 @@ const sanitizeCollection = (collections, collection) => { if (sanitized.upload) { if (!sanitized.upload.staticDir) sanitized.upload.staticDir = sanitized.slug; - if (!sanitized.upload.staticURL) sanitized.upload.staticURL = sanitized.slug; + if (!sanitized.upload.staticURL) sanitized.upload.staticURL = `/${sanitized.slug}`; if (!sanitized.admin.useAsTitle) sanitized.admin.useAsTitle = 'filename'; } From 4890e9dcf35e2ffa901d23e8faa60b01d03353a6 Mon Sep 17 00:00:00 2001 From: James Date: Sun, 19 Jul 2020 13:53:54 -0400 Subject: [PATCH 091/125] improves field validation --- src/fields/performFieldOperations.js | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/src/fields/performFieldOperations.js b/src/fields/performFieldOperations.js index e37cffd15..3c368323a 100644 --- a/src/fields/performFieldOperations.js +++ b/src/fields/performFieldOperations.js @@ -18,11 +18,15 @@ module.exports = async (config, entityConfig, operation) => { const hookPromises = []; const errors = []; - const createValidationPromise = async (data, field, path) => { + const createValidationPromise = async (newValue, existingValue, field, path) => { const hasCondition = field.admin && field.admin.condition; const shouldValidate = field.validate && !hasCondition; - const dataToValidate = data || field.defaultValue; - const result = shouldValidate ? await field.validate(dataToValidate, field) : true; + + let valueToValidate = newValue; + if (valueToValidate === undefined) valueToValidate = existingValue; + if (valueToValidate === undefined) valueToValidate = field.defaultValue; + + const result = shouldValidate ? await field.validate(valueToValidate, field) : true; if (!result || typeof result === 'string') { errors.push({ @@ -111,19 +115,23 @@ module.exports = async (config, entityConfig, operation) => { } } - if (operationName === 'create' || (operationName === 'update' && data[field.name] !== undefined)) { + if (operationName === 'create' || operationName === 'update') { if (field.type === 'array' || field.type === 'blocks') { - const hasRowsOfData = Array.isArray(data[field.name]); - const rowCount = hasRowsOfData ? data[field.name].length : 0; + const hasRowsOfNewData = Array.isArray(data[field.name]); + const newRowCount = hasRowsOfNewData ? data[field.name].length : 0; + // Handle cases of arrays being intentionally set to 0 if (data[field.name] === '0' || data[field.name] === 0 || data[field.name] === null) { const updatedData = data; updatedData[field.name] = []; } - validationPromises.push(createValidationPromise(rowCount, field, path)); - } else { - validationPromises.push(createValidationPromise(data[field.name], field, path)); + const hasRowsOfExistingData = Array.isArray(originalDoc[field.name]); + const existingRowCount = hasRowsOfExistingData ? originalDoc[field.name].length : 0; + + validationPromises.push(createValidationPromise(newRowCount, existingRowCount, field, path)); + } else if (field.name) { + validationPromises.push(createValidationPromise(data[field.name], originalDoc[field.name], field, path)); } } }); From 6ac4a9d2f65ee4184f206b4184d200eb908f420a Mon Sep 17 00:00:00 2001 From: James Date: Mon, 20 Jul 2020 09:15:06 -0400 Subject: [PATCH 092/125] implements hooks on relationship and upload fields --- demo/collections/CustomComponents/index.js | 5 - demo/collections/Hooks.js | 2 +- demo/collections/Media.js | 5 + src/collections/operations/find.js | 4 + src/collections/operations/findByID.js | 10 +- src/fields/performFieldOperations.js | 113 ++++++++++++++++++++- 6 files changed, 127 insertions(+), 12 deletions(-) diff --git a/demo/collections/CustomComponents/index.js b/demo/collections/CustomComponents/index.js index 1d6c5107c..566fc7192 100644 --- a/demo/collections/CustomComponents/index.js +++ b/demo/collections/CustomComponents/index.js @@ -15,11 +15,6 @@ module.exports = { required: true, unique: true, localized: true, - hooks: { - beforeCreate: (operation) => operation.value, - beforeUpdate: (operation) => operation.value, - afterRead: (operation) => operation.value, - }, }, { name: 'description', diff --git a/demo/collections/Hooks.js b/demo/collections/Hooks.js index 89c23110f..71b0f2984 100644 --- a/demo/collections/Hooks.js +++ b/demo/collections/Hooks.js @@ -90,7 +90,7 @@ module.exports = { localized: true, hooks: { afterRead: [ - (value) => (value ? value.toUpperCase() : null), + ({ value }) => (value ? value.toUpperCase() : null), ], }, }, diff --git a/demo/collections/Media.js b/demo/collections/Media.js index 9f53204ca..356813d18 100644 --- a/demo/collections/Media.js +++ b/demo/collections/Media.js @@ -41,6 +41,11 @@ module.exports = { type: 'text', required: true, localized: true, + hooks: { + afterRead: [ + ({ value }) => `${value} alt`, + ], + }, }, { name: 'sizes', diff --git a/src/collections/operations/find.js b/src/collections/operations/find.js index 5aa3d0747..f6c17a4b0 100644 --- a/src/collections/operations/find.js +++ b/src/collections/operations/find.js @@ -87,6 +87,10 @@ const find = async (args) => { optionsToExecute.options.autopopulate = { maxDepth: parseInt(depth, 10), }; + } else { + optionsToExecute.options.autopopulate = { + maxDepth: 2, + }; } } diff --git a/src/collections/operations/findByID.js b/src/collections/operations/findByID.js index ef0fc6df8..beca715f0 100644 --- a/src/collections/operations/findByID.js +++ b/src/collections/operations/findByID.js @@ -55,9 +55,7 @@ const findByID = async (args) => { // ///////////////////////////////////// const queryOptionsToExecute = { - options: { - autopopulate: false, - }, + autopopulate: false, }; // Only allow depth override within REST. @@ -65,9 +63,13 @@ const findByID = async (args) => { // as a full object will be returned instead of an ID string if (payloadAPI === 'REST') { if (depth && depth !== '0') { - queryOptionsToExecute.options.autopopulate = { + queryOptionsToExecute.autopopulate = { maxDepth: parseInt(depth, 10), }; + } else { + queryOptionsToExecute.autopopulate = { + maxDepth: 2, + }; } } diff --git a/src/fields/performFieldOperations.js b/src/fields/performFieldOperations.js index 3c368323a..6fec7a0e0 100644 --- a/src/fields/performFieldOperations.js +++ b/src/fields/performFieldOperations.js @@ -1,6 +1,6 @@ const { ValidationError } = require('../errors'); -module.exports = async (config, entityConfig, operation) => { +const performFieldOperations = async (config, entityConfig, operation) => { const { data: fullData, originalDoc: fullOriginalDoc, @@ -74,10 +74,116 @@ module.exports = async (config, entityConfig, operation) => { const createHookPromise = async (data, field) => { const resultingData = data; + const findRelatedCollection = (relation) => config.collections.find((collection) => collection.slug === relation); + // Todo: + // Check for afterRead operation and if found, + // Run relationship and upload-based hooks here + // Handle following scenarios: + // + // hasMany + // relationTo hasMany + // single + + if (hook === 'afterRead') { + if ((field.type === 'relationship' || field.type === 'upload')) { + const hasManyRelations = Array.isArray(field.relationTo); + + // If there are many related documents + if (field.hasMany && Array.isArray(data[field.name])) { + // Loop through relations + data[field.name].forEach(async (value, i) => { + let relation = field.relationTo; + + // If this field can be related to many collections, + // Set relationTo based on value + if (hasManyRelations && value && value.relationTo) { + relation = value.relationTo; + } + + if (relation) { + const relatedCollection = findRelatedCollection(relation); + + if (relatedCollection) { + let relatedDocumentData = data[field.name][i]; + let dataToHook = resultingData[field.name][i]; + + if (hasManyRelations) { + relatedDocumentData = data[field.name][i].value; + dataToHook = resultingData[field.name][i].value; + } + + // Only run hooks for populated sub documents - NOT IDs + if (relatedDocumentData && typeof relatedDocumentData !== 'string') { + // Perform field hooks on related collection + dataToHook = await performFieldOperations(config, relatedCollection, { + req, + data: relatedDocumentData, + hook: 'afterRead', + operationName: 'read', + }); + + await relatedCollection.hooks.afterRead.reduce(async (priorHook, currentHook) => { + await priorHook; + + dataToHook = await currentHook({ + req, + doc: relatedDocumentData, + }) || dataToHook; + }, Promise.resolve()); + } + } + } + }); + + // Otherwise, there is only one related document + } else { + let relation = field.relationTo; + + if (hasManyRelations && data[field.name] && data[field.name].relationTo) { + relation = data[field.name].relationTo; + } + + const relatedCollection = findRelatedCollection(relation); + + if (relatedCollection) { + let relatedDocumentData = data[field.name]; + let dataToHook = resultingData[field.name]; + + if (hasManyRelations) { + relatedDocumentData = data[field.name].value; + dataToHook = resultingData[field.name].value; + } + + // Only run hooks for populated sub documents - NOT IDs + if (relatedDocumentData && typeof relatedDocumentData !== 'string') { + // Perform field hooks on related collection + dataToHook = await performFieldOperations(config, relatedCollection, { + req, + data: relatedDocumentData, + hook: 'afterRead', + operationName: 'read', + }); + + await relatedCollection.hooks.afterRead.reduce(async (priorHook, currentHook) => { + await priorHook; + + dataToHook = await currentHook({ + req, + doc: relatedDocumentData, + }) || dataToHook; + }, Promise.resolve()); + } + } + } + } + } if (field.hooks && field.hooks[hook]) { field.hooks[hook].forEach(async (fieldHook) => { - resultingData[field.name] = await fieldHook(data[field.name]); + resultingData[field.name] = await fieldHook({ + value: data[field.name], + req, + }); }); } }; @@ -153,3 +259,6 @@ module.exports = async (config, entityConfig, operation) => { return fullData; }; + + +module.exports = performFieldOperations; From dd8f22db2a42185dcb98cbfe4493dd03c2b1fb9f Mon Sep 17 00:00:00 2001 From: James Date: Mon, 20 Jul 2020 09:15:23 -0400 Subject: [PATCH 093/125] 0.0.16 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 093c9a0d2..72fb2268f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@payloadcms/payload", - "version": "0.0.15", + "version": "0.0.16", "description": "CMS and Application Framework", "license": "ISC", "author": "Payload CMS LLC", From 0bfc10d55cd6ab3552d357b4e4eadb5bec648f2e Mon Sep 17 00:00:00 2001 From: James Date: Mon, 20 Jul 2020 12:44:29 -0400 Subject: [PATCH 094/125] passes depth to all afterRead performFieldOperations --- demo/blocks/Quote.js | 4 ++-- demo/globals/GlobalWithStrictAccess.js | 15 +++++++++++++++ demo/payload.config.js | 1 + src/auth/operations/register.js | 2 ++ src/auth/operations/update.js | 2 ++ src/collections/buildSchema.js | 4 +--- src/collections/operations/delete.js | 20 ++++++++++++++++++-- src/collections/operations/find.js | 22 ++-------------------- src/collections/operations/findByID.js | 23 ++--------------------- src/collections/operations/update.js | 2 ++ src/collections/requestHandlers/create.js | 1 + src/collections/requestHandlers/delete.js | 1 + src/collections/requestHandlers/update.js | 1 + src/globals/buildModel.js | 3 --- src/globals/operations/findOne.js | 19 +------------------ src/globals/operations/update.js | 2 ++ src/utilities/sanitizeConfig.js | 2 ++ 17 files changed, 55 insertions(+), 69 deletions(-) diff --git a/demo/blocks/Quote.js b/demo/blocks/Quote.js index 9d4d095a9..0b1386a8f 100644 --- a/demo/blocks/Quote.js +++ b/demo/blocks/Quote.js @@ -9,8 +9,8 @@ module.exports = { { name: 'author', label: 'Author', - type: 'text', - maxLength: 100, + type: 'relationship', + relationTo: 'public-users', required: true, }, { diff --git a/demo/globals/GlobalWithStrictAccess.js b/demo/globals/GlobalWithStrictAccess.js index 77d193d4b..ab15f5aa7 100644 --- a/demo/globals/GlobalWithStrictAccess.js +++ b/demo/globals/GlobalWithStrictAccess.js @@ -15,5 +15,20 @@ module.exports = { maxLength: 100, required: true, }, + { + name: 'relationship', + label: 'Test Relationship', + type: 'relationship', + relationTo: 'localized-posts', + hasMany: true, + required: true, + }, + { + name: 'singleRelationship', + label: 'Test Single Relationship', + type: 'relationship', + relationTo: 'localized-posts', + required: true, + }, ], }; diff --git a/demo/payload.config.js b/demo/payload.config.js index fecb7cc73..b96a2bc80 100644 --- a/demo/payload.config.js +++ b/demo/payload.config.js @@ -70,6 +70,7 @@ module.exports = { graphQL: '/graphql', graphQLPlayground: '/graphql-playground', }, + defaultDepth: 2, compression: {}, paths: { scss: path.resolve(__dirname, 'client/scss/overrides.scss'), diff --git a/src/auth/operations/register.js b/src/auth/operations/register.js index 7588c624d..10efd5f61 100644 --- a/src/auth/operations/register.js +++ b/src/auth/operations/register.js @@ -4,6 +4,7 @@ const performFieldOperations = require('../../fields/performFieldOperations'); const register = async (args) => { const { + depth, overrideAccess, config, collection: { @@ -81,6 +82,7 @@ const register = async (args) => { hook: 'afterRead', operationName: 'read', req, + depth, }); // ///////////////////////////////////// diff --git a/src/auth/operations/update.js b/src/auth/operations/update.js index a83451430..8ea33fe02 100644 --- a/src/auth/operations/update.js +++ b/src/auth/operations/update.js @@ -6,6 +6,7 @@ const performFieldOperations = require('../../fields/performFieldOperations'); const update = async (args) => { const { + depth, config, collection: { Model, @@ -115,6 +116,7 @@ const update = async (args) => { hook: 'afterRead', operationName: 'read', req, + depth, }); // ///////////////////////////////////// diff --git a/src/collections/buildSchema.js b/src/collections/buildSchema.js index 39f196a9c..6a09f62cf 100644 --- a/src/collections/buildSchema.js +++ b/src/collections/buildSchema.js @@ -1,5 +1,4 @@ const paginate = require('mongoose-paginate-v2'); -const autopopulate = require('mongoose-autopopulate'); const buildQueryPlugin = require('../mongoose/buildQuery'); const localizationPlugin = require('../localization/plugin'); const buildSchema = require('../mongoose/buildSchema'); @@ -8,8 +7,7 @@ const buildCollectionSchema = (collection, config, schemaOptions = {}) => { const schema = buildSchema(collection.fields, { timestamps: collection.timestamps, ...schemaOptions }); schema.plugin(paginate) - .plugin(buildQueryPlugin) - .plugin(autopopulate); + .plugin(buildQueryPlugin); if (config.localization) { schema.plugin(localizationPlugin, config.localization); diff --git a/src/collections/operations/delete.js b/src/collections/operations/delete.js index 386f0692a..83ca1db05 100644 --- a/src/collections/operations/delete.js +++ b/src/collections/operations/delete.js @@ -2,8 +2,11 @@ const fs = require('fs'); const { NotFound, Forbidden, ErrorDeletingFile } = require('../../errors'); const executeAccess = require('../../auth/executeAccess'); +const performFieldOperations = require('../../fields/performFieldOperations'); + const deleteQuery = async (args) => { const { + depth, collection: { Model, config: collectionConfig, @@ -14,6 +17,7 @@ const deleteQuery = async (args) => { locale, fallbackLocale, }, + config, } = args; // ///////////////////////////////////// @@ -90,7 +94,19 @@ const deleteQuery = async (args) => { } // ///////////////////////////////////// - // 4. Execute after collection hook + // 6. Execute field-level hooks and access + // ///////////////////////////////////// + + result = await performFieldOperations(config, collectionConfig, { + data: result, + hook: 'afterRead', + operationName: 'read', + req, + depth, + }); + + // ///////////////////////////////////// + // 7. Execute after collection hook // ///////////////////////////////////// await collectionConfig.hooks.afterDelete.reduce(async (priorHook, hook) => { @@ -100,7 +116,7 @@ const deleteQuery = async (args) => { }, Promise.resolve()); // ///////////////////////////////////// - // 5. Return results + // 8. Return results // ///////////////////////////////////// return result; diff --git a/src/collections/operations/find.js b/src/collections/operations/find.js index f6c17a4b0..519fcfcc0 100644 --- a/src/collections/operations/find.js +++ b/src/collections/operations/find.js @@ -6,8 +6,8 @@ const find = async (args) => { where, page, limit, - depth, config, + depth, collection: { Model, config: collectionConfig, @@ -16,7 +16,6 @@ const find = async (args) => { req: { locale, fallbackLocale, - payloadAPI, }, } = args; @@ -74,26 +73,8 @@ const find = async (args) => { limit: limit || 10, sort, collation: sort ? { locale: 'en' } : {}, // case-insensitive sort in MongoDB - options: { - autopopulate: false, - }, }; - // Only allow depth override within REST. - // If allowed in GraphQL, it would break resolvers - // as a full object will be returned instead of an ID string - if (payloadAPI === 'REST') { - if (depth && depth !== '0') { - optionsToExecute.options.autopopulate = { - maxDepth: parseInt(depth, 10), - }; - } else { - optionsToExecute.options.autopopulate = { - maxDepth: 2, - }; - } - } - let result = await Model.paginate(query, optionsToExecute); // ///////////////////////////////////// @@ -110,6 +91,7 @@ const find = async (args) => { const data = doc.toJSON({ virtuals: true }); return performFieldOperations(config, collectionConfig, { + depth, data, req, hook: 'afterRead', diff --git a/src/collections/operations/findByID.js b/src/collections/operations/findByID.js index beca715f0..d8e5aa70e 100644 --- a/src/collections/operations/findByID.js +++ b/src/collections/operations/findByID.js @@ -15,7 +15,6 @@ const findByID = async (args) => { req: { locale, fallbackLocale, - payloadAPI, }, } = args; @@ -54,26 +53,7 @@ const findByID = async (args) => { // 3. Perform database operation // ///////////////////////////////////// - const queryOptionsToExecute = { - autopopulate: false, - }; - - // Only allow depth override within REST. - // If allowed in GraphQL, it would break resolvers - // as a full object will be returned instead of an ID string - if (payloadAPI === 'REST') { - if (depth && depth !== '0') { - queryOptionsToExecute.autopopulate = { - maxDepth: parseInt(depth, 10), - }; - } else { - queryOptionsToExecute.autopopulate = { - maxDepth: 2, - }; - } - } - - let result = await Model.findOne(query, {}, queryOptionsToExecute); + let result = await Model.findOne(query, {}); if (!result && !hasWhereAccess) throw new NotFound(); if (!result && hasWhereAccess) throw new Forbidden(); @@ -89,6 +69,7 @@ const findByID = async (args) => { // ///////////////////////////////////// result = await performFieldOperations(config, collectionConfig, { + depth, req, data: result, hook: 'afterRead', diff --git a/src/collections/operations/update.js b/src/collections/operations/update.js index 9723e472e..aeab18284 100644 --- a/src/collections/operations/update.js +++ b/src/collections/operations/update.js @@ -12,6 +12,7 @@ const resizeAndSave = require('../../uploads/imageResizer'); const update = async (args) => { const { config, + depth, collection: { Model, config: collectionConfig, @@ -158,6 +159,7 @@ const update = async (args) => { hook: 'afterRead', operationName: 'read', req, + depth, }); // ///////////////////////////////////// diff --git a/src/collections/requestHandlers/create.js b/src/collections/requestHandlers/create.js index 7b967beec..06b2ce449 100644 --- a/src/collections/requestHandlers/create.js +++ b/src/collections/requestHandlers/create.js @@ -9,6 +9,7 @@ const createHandler = (config) => async (req, res, next) => { collection: req.collection, config, data: req.body, + depth: req.query.depth, }); return res.status(httpStatus.CREATED).json({ diff --git a/src/collections/requestHandlers/delete.js b/src/collections/requestHandlers/delete.js index 9a0dd2f28..30a7230de 100644 --- a/src/collections/requestHandlers/delete.js +++ b/src/collections/requestHandlers/delete.js @@ -9,6 +9,7 @@ const deleteHandler = (config) => async (req, res, next) => { collection: req.collection, config, id: req.params.id, + depth: req.query.depth, }); if (!doc) { diff --git a/src/collections/requestHandlers/update.js b/src/collections/requestHandlers/update.js index 903c7c9ff..bf81a199e 100644 --- a/src/collections/requestHandlers/update.js +++ b/src/collections/requestHandlers/update.js @@ -10,6 +10,7 @@ const updateHandler = (config) => async (req, res, next) => { config, id: req.params.id, data: req.body, + depth: req.query.depth, }); return res.status(httpStatus.OK).json({ diff --git a/src/globals/buildModel.js b/src/globals/buildModel.js index d69b6f96e..af2f714b3 100644 --- a/src/globals/buildModel.js +++ b/src/globals/buildModel.js @@ -1,5 +1,4 @@ const mongoose = require('mongoose'); -const autopopulate = require('mongoose-autopopulate'); const mongooseHidden = require('mongoose-hidden')(); const buildSchema = require('../mongoose/buildSchema'); const localizationPlugin = require('../localization/plugin'); @@ -12,7 +11,6 @@ const buildModel = (config) => { globalsSchema.plugin(localizationPlugin, config.localization); } - globalsSchema.plugin(autopopulate); globalsSchema.plugin(mongooseHidden); const Globals = mongoose.model('globals', globalsSchema); @@ -24,7 +22,6 @@ const buildModel = (config) => { globalSchema.plugin(localizationPlugin, config.localization); } - globalSchema.plugin(autopopulate); globalSchema.plugin(mongooseHidden); Globals.discriminator(globalConfig.slug, globalSchema); diff --git a/src/globals/operations/findOne.js b/src/globals/operations/findOne.js index aa7ede589..9c0272714 100644 --- a/src/globals/operations/findOne.js +++ b/src/globals/operations/findOne.js @@ -8,7 +8,6 @@ const findOne = async (args) => { Model, req, req: { - payloadAPI, locale, fallbackLocale, }, @@ -32,23 +31,6 @@ const findOne = async (args) => { // 3. Perform database operation // ///////////////////////////////////// - const queryOptionsToExecute = { - options: {}, - }; - - // Only allow depth override within REST. - // If allowed in GraphQL, it would break resolvers - // as a full object will be returned instead of an ID string - if (payloadAPI === 'REST') { - if (depth && depth !== '0') { - queryOptionsToExecute.options.autopopulate = { - maxDepth: parseInt(depth, 10), - }; - } else { - queryOptionsToExecute.options.autopopulate = false; - } - } - let doc = await Model.findOne({ globalType: slug }); if (!doc) { @@ -71,6 +53,7 @@ const findOne = async (args) => { hook: 'afterRead', operationName: 'read', req, + depth, }); // ///////////////////////////////////// diff --git a/src/globals/operations/update.js b/src/globals/operations/update.js index e8ecfc86c..1c2e2486c 100644 --- a/src/globals/operations/update.js +++ b/src/globals/operations/update.js @@ -14,6 +14,7 @@ const update = async (args) => { locale, fallbackLocale, }, + depth, } = args; // ///////////////////////////////////// @@ -91,6 +92,7 @@ const update = async (args) => { hook: 'afterRead', operationName: 'read', req, + depth, }); // ///////////////////////////////////// diff --git a/src/utilities/sanitizeConfig.js b/src/utilities/sanitizeConfig.js index daf168093..77e22cae5 100644 --- a/src/utilities/sanitizeConfig.js +++ b/src/utilities/sanitizeConfig.js @@ -5,6 +5,8 @@ const sanitizeGlobals = require('../globals/sanitize'); const sanitizeConfig = (config) => { const sanitizedConfig = { ...config }; + if (sanitizedConfig.defaultDepth === undefined) sanitizedConfig.defaultDepth = 2; + sanitizedConfig.collections = sanitizedConfig.collections.map((collection) => sanitizeCollection(sanitizedConfig.collections, collection)); if (sanitizedConfig.globals) { From 85bcbfd918f1fec52384a133a7caa657bf3f39b4 Mon Sep 17 00:00:00 2001 From: James Date: Mon, 20 Jul 2020 16:11:22 -0400 Subject: [PATCH 095/125] removes autopopulate, adds manual population --- demo/globals/GlobalWithStrictAccess.js | 3 +- package.json | 1 - src/auth/executeAccess.js | 5 +- src/auth/operations/access.js | 7 +- src/auth/operations/forgotPassword.js | 8 +- src/auth/operations/index.js | 25 --- src/auth/operations/init.js | 4 +- src/auth/operations/login.js | 7 +- src/auth/operations/logout.js | 7 +- src/auth/operations/me.js | 6 +- src/auth/operations/refresh.js | 6 +- src/auth/operations/register.js | 12 +- src/auth/operations/registerFirstUser.js | 4 +- src/auth/operations/resetPassword.js | 7 +- src/auth/operations/update.js | 12 +- src/auth/requestHandlers/access.js | 8 +- src/auth/requestHandlers/forgotPassword.js | 9 +- src/auth/requestHandlers/index.js | 25 --- src/auth/requestHandlers/init.js | 8 +- src/auth/requestHandlers/login.js | 8 +- src/auth/requestHandlers/logout.js | 9 +- src/auth/requestHandlers/me.js | 10 +- src/auth/requestHandlers/refresh.js | 10 +- src/auth/requestHandlers/register.js | 10 +- src/auth/requestHandlers/registerFirstUser.js | 11 +- src/auth/requestHandlers/resetPassword.js | 10 +- src/auth/requestHandlers/update.js | 10 +- src/auth/routes.js | 79 -------- src/collections/graphql/resolvers/findByID.js | 4 +- src/collections/init.js | 83 ++++++++- src/collections/operations/create.js | 13 +- src/collections/operations/delete.js | 9 +- src/collections/operations/find.js | 24 +-- src/collections/operations/findByID.js | 23 ++- src/collections/operations/index.js | 13 -- src/collections/operations/update.js | 10 +- src/collections/requestHandlers/create.js | 10 +- src/collections/requestHandlers/delete.js | 8 +- src/collections/requestHandlers/find.js | 10 +- src/collections/requestHandlers/findByID.js | 11 +- src/collections/requestHandlers/index.js | 13 -- src/collections/requestHandlers/update.js | 10 +- src/collections/routes.js | 27 --- src/fields/performFieldOperations.js | 170 +++++++++++------- src/globals/init.js | 13 +- src/globals/operations/findOne.js | 11 +- src/globals/operations/index.js | 7 - src/globals/operations/update.js | 13 +- src/globals/requestHandlers/findOne.js | 37 ++-- src/globals/requestHandlers/index.js | 7 - src/globals/requestHandlers/update.js | 38 ++-- src/globals/routes.js | 19 -- src/graphql/schema/buildObjectType.js | 5 +- src/index.js | 40 +++-- src/init/bindOperations.js | 53 ++++++ src/init/bindRequestHandlers.js | 53 ++++++ 56 files changed, 527 insertions(+), 528 deletions(-) delete mode 100644 src/auth/operations/index.js delete mode 100644 src/auth/requestHandlers/index.js delete mode 100644 src/auth/routes.js delete mode 100644 src/collections/operations/index.js delete mode 100644 src/collections/requestHandlers/index.js delete mode 100644 src/collections/routes.js delete mode 100644 src/globals/operations/index.js delete mode 100644 src/globals/requestHandlers/index.js delete mode 100644 src/globals/routes.js create mode 100644 src/init/bindOperations.js create mode 100644 src/init/bindRequestHandlers.js diff --git a/demo/globals/GlobalWithStrictAccess.js b/demo/globals/GlobalWithStrictAccess.js index ab15f5aa7..1361bfaae 100644 --- a/demo/globals/GlobalWithStrictAccess.js +++ b/demo/globals/GlobalWithStrictAccess.js @@ -5,7 +5,8 @@ module.exports = { label: 'Global with Strict Access', access: { update: ({ req: { user } }) => checkRole(['admin'], user), - read: ({ req: { user } }) => checkRole(['admin'], user), + // read: ({ req: { user } }) => checkRole(['admin'], user), + read: () => true, }, fields: [ { diff --git a/package.json b/package.json index 72fb2268f..0cb3ad825 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,6 @@ "mkdirp": "^0.5.1", "mongodb-memory-server": "^6.5.2", "mongoose": "^5.8.9", - "mongoose-autopopulate": "^0.11.0", "mongoose-hidden": "^1.8.1", "mongoose-paginate-v2": "^1.3.6", "node-sass": "^4.13.1", diff --git a/src/auth/executeAccess.js b/src/auth/executeAccess.js index 322d8bb2e..2a161d580 100644 --- a/src/auth/executeAccess.js +++ b/src/auth/executeAccess.js @@ -5,7 +5,7 @@ const executeAccess = async (operation, access) => { const result = await access(operation); if (!result) { - throw new Forbidden(); + if (!operation.disableErrors) throw new Forbidden(); } return result; @@ -15,7 +15,8 @@ const executeAccess = async (operation, access) => { return true; } - throw new Forbidden(); + if (!operation.disableErrors) throw new Forbidden(); + return false; }; module.exports = executeAccess; diff --git a/src/auth/operations/access.js b/src/auth/operations/access.js index 3e460d744..6e3ab6a95 100644 --- a/src/auth/operations/access.js +++ b/src/auth/operations/access.js @@ -1,8 +1,9 @@ const allOperations = ['create', 'read', 'update', 'delete']; -const accessOperation = async (args) => { +async function accessOperation(args) { + const { config } = this; + const { - config, req, req: { user }, } = args; @@ -101,6 +102,6 @@ const accessOperation = async (args) => { await Promise.all(promises); return results; -}; +} module.exports = accessOperation; diff --git a/src/auth/operations/forgotPassword.js b/src/auth/operations/forgotPassword.js index d9dc73190..3c287ec65 100644 --- a/src/auth/operations/forgotPassword.js +++ b/src/auth/operations/forgotPassword.js @@ -1,7 +1,9 @@ const crypto = require('crypto'); const { APIError } = require('../../errors'); -const forgotPassword = async (args) => { +async function forgotPassword(args) { + const { config, email } = this; + if (!Object.prototype.hasOwnProperty.call(args.data, 'email')) { throw new APIError('Missing email.'); } @@ -26,9 +28,7 @@ const forgotPassword = async (args) => { collection: { Model, }, - config, data, - email, } = options; let token = await crypto.randomBytes(20); @@ -66,6 +66,6 @@ const forgotPassword = async (args) => { if (typeof afterForgotPassword === 'function') { await afterForgotPassword(options); } -}; +} module.exports = forgotPassword; diff --git a/src/auth/operations/index.js b/src/auth/operations/index.js deleted file mode 100644 index f9da1162a..000000000 --- a/src/auth/operations/index.js +++ /dev/null @@ -1,25 +0,0 @@ -const login = require('./login'); -const refresh = require('./refresh'); -const register = require('./register'); -const init = require('./init'); -const forgotPassword = require('./forgotPassword'); -const resetPassword = require('./resetPassword'); -const registerFirstUser = require('./registerFirstUser'); -const update = require('./update'); -const access = require('./access'); -const me = require('./me'); -const logout = require('./logout'); - -module.exports = { - login, - refresh, - init, - register, - forgotPassword, - update, - resetPassword, - registerFirstUser, - access, - me, - logout, -}; diff --git a/src/auth/operations/init.js b/src/auth/operations/init.js index ecb46d3b7..75b05143f 100644 --- a/src/auth/operations/init.js +++ b/src/auth/operations/init.js @@ -1,4 +1,4 @@ -const init = async (args) => { +async function init(args) { const { Model, } = args; @@ -8,6 +8,6 @@ const init = async (args) => { if (count >= 1) return true; return false; -}; +} module.exports = init; diff --git a/src/auth/operations/login.js b/src/auth/operations/login.js index 83de91111..8e6ac89a4 100644 --- a/src/auth/operations/login.js +++ b/src/auth/operations/login.js @@ -1,8 +1,8 @@ const jwt = require('jsonwebtoken'); const { AuthenticationError } = require('../../errors'); -const login = async (args) => { - // Await validation here +async function login(args) { + const { config } = this; const options = { ...args }; @@ -21,7 +21,6 @@ const login = async (args) => { Model, config: collectionConfig, }, - config, data, } = options; @@ -90,6 +89,6 @@ const login = async (args) => { // ///////////////////////////////////// return token; -}; +} module.exports = login; diff --git a/src/auth/operations/logout.js b/src/auth/operations/logout.js index 44963edcb..5cf893c7c 100644 --- a/src/auth/operations/logout.js +++ b/src/auth/operations/logout.js @@ -1,6 +1,7 @@ -const logout = async (args) => { +async function logout(args) { + const { config } = this; + const { - config, collection: { config: collectionConfig, }, @@ -28,6 +29,6 @@ const logout = async (args) => { res.cookie(`${config.cookiePrefix}-token`, '', cookieOptions); return 'Logged out successfully.'; -}; +} module.exports = logout; diff --git a/src/auth/operations/me.js b/src/auth/operations/me.js index 40438a22f..876192e79 100644 --- a/src/auth/operations/me.js +++ b/src/auth/operations/me.js @@ -1,8 +1,8 @@ const jwt = require('jsonwebtoken'); const getExtractJWT = require('../getExtractJWT'); -const me = async ({ req, config }) => { - const extractJWT = getExtractJWT(config); +async function me({ req }) { + const extractJWT = getExtractJWT(this.config); if (req.user) { const response = { @@ -26,6 +26,6 @@ const me = async ({ req, config }) => { return { user: null, }; -}; +} module.exports = me; diff --git a/src/auth/operations/refresh.js b/src/auth/operations/refresh.js index b84fb16fe..e1761f34e 100644 --- a/src/auth/operations/refresh.js +++ b/src/auth/operations/refresh.js @@ -1,9 +1,10 @@ const jwt = require('jsonwebtoken'); const { Forbidden } = require('../../errors'); -const refresh = async (args) => { +async function refresh(args) { // Await validation here + const { secret, cookiePrefix } = this.config; let options = { ...args }; // ///////////////////////////////////// @@ -20,7 +21,6 @@ const refresh = async (args) => { // 2. Perform refresh // ///////////////////////////////////// - const { secret, cookiePrefix } = options.config; const opts = {}; opts.expiresIn = options.collection.config.auth.tokenExpiration; @@ -71,6 +71,6 @@ const refresh = async (args) => { refreshedToken, user: payload, }; -}; +} module.exports = refresh; diff --git a/src/auth/operations/register.js b/src/auth/operations/register.js index 10efd5f61..4bbc3054a 100644 --- a/src/auth/operations/register.js +++ b/src/auth/operations/register.js @@ -1,12 +1,12 @@ const passport = require('passport'); const executeAccess = require('../executeAccess'); -const performFieldOperations = require('../../fields/performFieldOperations'); -const register = async (args) => { +async function register(args) { + const { config } = this; + const { depth, overrideAccess, - config, collection: { Model, config: collectionConfig, @@ -45,7 +45,7 @@ const register = async (args) => { // 3. Execute field-level hooks, access, and validation // ///////////////////////////////////// - data = await performFieldOperations(config, collectionConfig, { + data = await this.performFieldOperations(collectionConfig, { data, hook: 'beforeCreate', operationName: 'create', @@ -77,7 +77,7 @@ const register = async (args) => { // 7. Execute field-level hooks and access // ///////////////////////////////////// - result = await performFieldOperations(config, collectionConfig, { + result = await this.performFieldOperations(collectionConfig, { data: result, hook: 'afterRead', operationName: 'read', @@ -103,6 +103,6 @@ const register = async (args) => { // ///////////////////////////////////// return result; -}; +} module.exports = register; diff --git a/src/auth/operations/registerFirstUser.js b/src/auth/operations/registerFirstUser.js index 88c181ba0..dfa163a0d 100644 --- a/src/auth/operations/registerFirstUser.js +++ b/src/auth/operations/registerFirstUser.js @@ -2,7 +2,7 @@ const register = require('./register'); const login = require('./login'); const { Forbidden } = require('../../errors'); -const registerFirstUser = async (args) => { +async function registerFirstUser(args) { const { collection: { Model, @@ -40,6 +40,6 @@ const registerFirstUser = async (args) => { message: 'Registered successfully. Welcome to Payload!', user: result, }; -}; +} module.exports = registerFirstUser; diff --git a/src/auth/operations/resetPassword.js b/src/auth/operations/resetPassword.js index 7994b9256..1914c071e 100644 --- a/src/auth/operations/resetPassword.js +++ b/src/auth/operations/resetPassword.js @@ -1,7 +1,9 @@ const jwt = require('jsonwebtoken'); const { APIError } = require('../../errors'); -const resetPassword = async (args) => { +async function resetPassword(args) { + const { config } = this; + if (!Object.prototype.hasOwnProperty.call(args.data, 'token') || !Object.prototype.hasOwnProperty.call(args.data, 'password')) { throw new APIError('Missing required data.'); @@ -28,7 +30,6 @@ const resetPassword = async (args) => { Model, config: collectionConfig, }, - config, data, } = options; @@ -85,6 +86,6 @@ const resetPassword = async (args) => { // ///////////////////////////////////// return token; -}; +} module.exports = resetPassword; diff --git a/src/auth/operations/update.js b/src/auth/operations/update.js index 8ea33fe02..5ec408804 100644 --- a/src/auth/operations/update.js +++ b/src/auth/operations/update.js @@ -2,12 +2,12 @@ const deepmerge = require('deepmerge'); const overwriteMerge = require('../../utilities/overwriteMerge'); const { NotFound, Forbidden } = require('../../errors'); const executeAccess = require('../executeAccess'); -const performFieldOperations = require('../../fields/performFieldOperations'); -const update = async (args) => { +async function update(args) { + const { config } = this; + const { depth, - config, collection: { Model, config: collectionConfig, @@ -77,7 +77,7 @@ const update = async (args) => { // 4. Execute field-level hooks, access, and validation // ///////////////////////////////////// - data = await performFieldOperations(config, collectionConfig, { + data = await this.performFieldOperations(collectionConfig, { data, req, hook: 'beforeUpdate', @@ -111,7 +111,7 @@ const update = async (args) => { // 7. Execute field-level hooks and access // ///////////////////////////////////// - user = performFieldOperations(config, collectionConfig, { + user = this.performFieldOperations(collectionConfig, { data: user, hook: 'afterRead', operationName: 'read', @@ -137,6 +137,6 @@ const update = async (args) => { // ///////////////////////////////////// return user; -}; +} module.exports = update; diff --git a/src/auth/requestHandlers/access.js b/src/auth/requestHandlers/access.js index e94fe8e70..d4883ae42 100644 --- a/src/auth/requestHandlers/access.js +++ b/src/auth/requestHandlers/access.js @@ -1,11 +1,9 @@ const httpStatus = require('http-status'); -const { access } = require('../operations'); -const policiesHandler = (config) => async (req, res, next) => { +async function policiesHandler(req, res, next) { try { - const accessResults = await access({ + const accessResults = await this.operations.collections.auth.access({ req, - config, }); return res.status(httpStatus.OK) @@ -13,6 +11,6 @@ const policiesHandler = (config) => async (req, res, next) => { } catch (error) { return next(error); } -}; +} module.exports = policiesHandler; diff --git a/src/auth/requestHandlers/forgotPassword.js b/src/auth/requestHandlers/forgotPassword.js index 742c7f614..05fa8daa5 100644 --- a/src/auth/requestHandlers/forgotPassword.js +++ b/src/auth/requestHandlers/forgotPassword.js @@ -1,14 +1,11 @@ const httpStatus = require('http-status'); -const { forgotPassword } = require('../operations'); -const forgotPasswordHandler = (config, email) => async (req, res, next) => { +async function forgotPasswordHandler(req, res, next) { try { - await forgotPassword({ + await this.operations.collections.auth.forgotPassword({ req, collection: req.collection, - config, data: req.body, - email, }); return res.status(httpStatus.OK) @@ -18,6 +15,6 @@ const forgotPasswordHandler = (config, email) => async (req, res, next) => { } catch (error) { return next(error); } -}; +} module.exports = forgotPasswordHandler; diff --git a/src/auth/requestHandlers/index.js b/src/auth/requestHandlers/index.js deleted file mode 100644 index 96ce73a39..000000000 --- a/src/auth/requestHandlers/index.js +++ /dev/null @@ -1,25 +0,0 @@ -const login = require('./login'); -const me = require('./me'); -const refresh = require('./refresh'); -const register = require('./register'); -const init = require('./init'); -const forgotPassword = require('./forgotPassword'); -const resetPassword = require('./resetPassword'); -const registerFirstUser = require('./registerFirstUser'); -const update = require('./update'); -const access = require('./access'); -const logout = require('./logout'); - -module.exports = { - login, - logout, - me, - refresh, - init, - register, - forgotPassword, - registerFirstUser, - resetPassword, - update, - access, -}; diff --git a/src/auth/requestHandlers/init.js b/src/auth/requestHandlers/init.js index a7891ffb0..272d34d82 100644 --- a/src/auth/requestHandlers/init.js +++ b/src/auth/requestHandlers/init.js @@ -1,12 +1,10 @@ -const { init } = require('../operations'); - -const initHandler = async (req, res, next) => { +async function initHandler(req, res, next) { try { - const initialized = await init({ Model: req.collection.Model }); + const initialized = await this.operations.collections.auth.init({ Model: req.collection.Model }); return res.status(200).json({ initialized }); } catch (error) { return next(error); } -}; +} module.exports = initHandler; diff --git a/src/auth/requestHandlers/login.js b/src/auth/requestHandlers/login.js index 0bf315274..7cff1d5e5 100644 --- a/src/auth/requestHandlers/login.js +++ b/src/auth/requestHandlers/login.js @@ -1,13 +1,11 @@ const httpStatus = require('http-status'); -const { login } = require('../operations'); -const loginHandler = (config) => async (req, res, next) => { +async function loginHandler(req, res, next) { try { - const token = await login({ + const token = await this.operations.collections.auth.login({ req, res, collection: req.collection, - config, data: req.body, }); @@ -19,6 +17,6 @@ const loginHandler = (config) => async (req, res, next) => { } catch (error) { return next(error); } -}; +} module.exports = loginHandler; diff --git a/src/auth/requestHandlers/logout.js b/src/auth/requestHandlers/logout.js index 857be4757..e181764c2 100644 --- a/src/auth/requestHandlers/logout.js +++ b/src/auth/requestHandlers/logout.js @@ -1,9 +1,6 @@ -const { logout } = require('../operations'); - -const logoutHandler = (config) => async (req, res, next) => { +async function logoutHandler(req, res, next) { try { - const message = await logout({ - config, + const message = await this.operations.collections.auth.logout({ collection: req.collection, res, req, @@ -13,6 +10,6 @@ const logoutHandler = (config) => async (req, res, next) => { } catch (error) { return next(error); } -}; +} module.exports = logoutHandler; diff --git a/src/auth/requestHandlers/me.js b/src/auth/requestHandlers/me.js index b3847a31d..c26c159d9 100644 --- a/src/auth/requestHandlers/me.js +++ b/src/auth/requestHandlers/me.js @@ -1,12 +1,10 @@ -const { me } = require('../operations'); - -const meHandler = config => async (req, res, next) => { +async function me(req, res, next) { try { - const response = await me({ req, config }); + const response = await this.operations.collections.auth.me({ req }); return res.status(200).json(response); } catch (err) { return next(err); } -}; +} -module.exports = meHandler; +module.exports = me; diff --git a/src/auth/requestHandlers/refresh.js b/src/auth/requestHandlers/refresh.js index ec3558b3d..8e2f2ebc2 100644 --- a/src/auth/requestHandlers/refresh.js +++ b/src/auth/requestHandlers/refresh.js @@ -1,16 +1,14 @@ -const { refresh } = require('../operations'); const getExtractJWT = require('../getExtractJWT'); -const refreshHandler = config => async (req, res, next) => { +async function refreshHandler(req, res, next) { try { - const extractJWT = getExtractJWT(config); + const extractJWT = getExtractJWT(this.config); const token = extractJWT(req); - const result = await refresh({ + const result = await this.operations.collections.auth.refresh({ req, res, collection: req.collection, - config, token, }); @@ -21,6 +19,6 @@ const refreshHandler = config => async (req, res, next) => { } catch (error) { return next(error); } -}; +} module.exports = refreshHandler; diff --git a/src/auth/requestHandlers/register.js b/src/auth/requestHandlers/register.js index 9757114ae..2621cbefb 100644 --- a/src/auth/requestHandlers/register.js +++ b/src/auth/requestHandlers/register.js @@ -1,11 +1,9 @@ const httpStatus = require('http-status'); const formatSuccessResponse = require('../../express/responses/formatSuccess'); -const { register } = require('../operations'); -const registerHandler = (config) => async (req, res, next) => { +async function register(req, res, next) { try { - const user = await register({ - config, + const user = await this.operations.collections.auth.register({ collection: req.collection, req, data: req.body, @@ -18,6 +16,6 @@ const registerHandler = (config) => async (req, res, next) => { } catch (error) { return next(error); } -}; +} -module.exports = registerHandler; +module.exports = register; diff --git a/src/auth/requestHandlers/registerFirstUser.js b/src/auth/requestHandlers/registerFirstUser.js index 23b2cba91..1ca01af6f 100644 --- a/src/auth/requestHandlers/registerFirstUser.js +++ b/src/auth/requestHandlers/registerFirstUser.js @@ -1,11 +1,8 @@ -const { registerFirstUser } = require('../operations'); - -const registerFirstUserHandler = (config) => async (req, res, next) => { +async function registerFirstUser(req, res, next) { try { - const firstUser = await registerFirstUser({ + const firstUser = await this.operations.collections.auth.registerFirstUser({ req, res, - config, collection: req.collection, data: req.body, }); @@ -14,6 +11,6 @@ const registerFirstUserHandler = (config) => async (req, res, next) => { } catch (error) { return next(error); } -}; +} -module.exports = registerFirstUserHandler; +module.exports = registerFirstUser; diff --git a/src/auth/requestHandlers/resetPassword.js b/src/auth/requestHandlers/resetPassword.js index 73665abca..a35f24df3 100644 --- a/src/auth/requestHandlers/resetPassword.js +++ b/src/auth/requestHandlers/resetPassword.js @@ -1,12 +1,10 @@ const httpStatus = require('http-status'); -const { resetPassword } = require('../operations'); -const resetPasswordHandler = config => async (req, res, next) => { +async function resetPassword(req, res, next) { try { - const token = await resetPassword({ + const token = await this.operations.collections.auth.resetPassword({ req, collection: req.collection, - config, data: req.body, }); @@ -18,6 +16,6 @@ const resetPasswordHandler = config => async (req, res, next) => { } catch (error) { return next(error); } -}; +} -module.exports = resetPasswordHandler; +module.exports = resetPassword; diff --git a/src/auth/requestHandlers/update.js b/src/auth/requestHandlers/update.js index a4927f355..778400a0f 100644 --- a/src/auth/requestHandlers/update.js +++ b/src/auth/requestHandlers/update.js @@ -1,14 +1,12 @@ const httpStatus = require('http-status'); const formatSuccessResponse = require('../../express/responses/formatSuccess'); -const { update } = require('../operations'); -const updateHandler = (config) => async (req, res, next) => { +async function update(req, res, next) { try { - const user = await update({ + const user = await this.operations.collections.auth.update({ req, data: req.body, collection: req.collection, - config, id: req.params.id, }); @@ -19,6 +17,6 @@ const updateHandler = (config) => async (req, res, next) => { } catch (error) { return next(error); } -}; +} -module.exports = updateHandler; +module.exports = update; diff --git a/src/auth/routes.js b/src/auth/routes.js deleted file mode 100644 index f0a2088b3..000000000 --- a/src/auth/routes.js +++ /dev/null @@ -1,79 +0,0 @@ -const express = require('express'); -const bindCollectionMiddleware = require('../collections/bindCollection'); - -const { - init, - login, - logout, - refresh, - me, - register, - registerFirstUser, - forgotPassword, - resetPassword, - update, -} = require('./requestHandlers'); - -const { - find, - findByID, - deleteHandler, -} = require('../collections/requestHandlers'); - -const router = express.Router(); - -const authRoutes = (collection, config, sendEmail) => { - const { slug } = collection.config; - - router.all('*', - bindCollectionMiddleware(collection)); - - router - .route(`/${slug}/init`) - .get(init); - - router - .route(`/${slug}/login`) - .post(login(config)); - - router - .route(`/${slug}/logout`) - .get(logout(config)); - - router - .route(`/${slug}/refresh-token`) - .post(refresh(config)); - - router - .route(`/${slug}/me`) - .get(me(config)); - - router - .route(`/${slug}/first-register`) - .post(registerFirstUser(config)); - - router - .route(`/${slug}/forgot-password`) - .post(forgotPassword(config, sendEmail)); - - router - .route(`${slug}/reset-password`) - .post(resetPassword); - - router - .route(`/${slug}/register`) - .post(register(config)); - - router - .route(`/${slug}`) - .get(find(config)); - - router.route(`/${slug}/:id`) - .get(findByID(config)) - .put(update(config)) - .delete(deleteHandler(config)); - - return router; -}; - -module.exports = authRoutes; diff --git a/src/collections/graphql/resolvers/findByID.js b/src/collections/graphql/resolvers/findByID.js index 880a432f1..4bae467c0 100644 --- a/src/collections/graphql/resolvers/findByID.js +++ b/src/collections/graphql/resolvers/findByID.js @@ -1,7 +1,5 @@ /* eslint-disable no-param-reassign */ -const { findByID } = require('../../operations'); - -const findByIDResolver = (config, collection) => async (_, args, context) => { +const findByIDResolver = (collection) => async (_, args, context) => { if (args.locale) context.req.locale = args.locale; if (args.fallbackLocale) context.req.fallbackLocale = args.fallbackLocale; diff --git a/src/collections/init.js b/src/collections/init.js index eb29a8b3b..8d3b92f0b 100644 --- a/src/collections/init.js +++ b/src/collections/init.js @@ -1,4 +1,5 @@ const mongoose = require('mongoose'); +const express = require('express'); const mongooseHidden = require('mongoose-hidden')({ hidden: { salt: true, hash: true, _id: true, __v: true, @@ -9,9 +10,8 @@ const passport = require('passport'); const passportLocalMongoose = require('passport-local-mongoose'); const LocalStrategy = require('passport-local').Strategy; const apiKeyStrategy = require('../auth/strategies/apiKey'); -const collectionRoutes = require('./routes'); const buildSchema = require('./buildSchema'); -const authRoutes = require('../auth/routes'); +const bindCollectionMiddleware = require('./bindCollection'); function registerCollections() { this.config.collections = this.config.collections.map((collection) => { @@ -30,6 +30,27 @@ function registerCollections() { config: formattedCollection, }; + const router = express.Router(); + const { slug } = collection; + + router.all(`/${slug}*`, bindCollectionMiddleware(this.collections[formattedCollection.slug])); + + const { + create, + find, + update, + findByID, + delete: deleteHandler, + } = this.requestHandlers.collections; + + router.route(`/${slug}`) + .get(find) + .put(update); + + router.route(`/${slug}/:id`) + .get(findByID) + .delete(deleteHandler); + if (collection.auth) { const AuthCollection = this.collections[formattedCollection.slug]; passport.use(new LocalStrategy(AuthCollection.Model.authenticate())); @@ -38,11 +59,65 @@ function registerCollections() { passport.use(`${AuthCollection.config.slug}-api-key`, apiKeyStrategy(AuthCollection)); } - this.router.use(authRoutes(AuthCollection, this.config, this.sendEmail)); + const { + init, + login, + logout, + refresh, + me, + register, + registerFirstUser, + forgotPassword, + resetPassword, + update: authUpdate, + } = this.requestHandlers.collections.auth; + + router + .route(`/${slug}/init`) + .get(init); + + router + .route(`/${slug}/login`) + .post(login); + + router + .route(`/${slug}/logout`) + .get(logout); + + router + .route(`/${slug}/refresh-token`) + .post(refresh); + + router + .route(`/${slug}/me`) + .get(me); + + router + .route(`/${slug}/first-register`) + .post(registerFirstUser); + + router + .route(`/${slug}/forgot-password`) + .post(forgotPassword); + + router + .route(`${slug}/reset-password`) + .post(resetPassword); + + router + .route(`/${slug}/register`) + .post(register); + + router.route(`/${slug}/:id`) + .put(authUpdate); } else { - this.router.use(collectionRoutes(this.collections[formattedCollection.slug], this.config)); + router.route(`/${slug}`) + .get(find) + .post(create); } + this.router.use(router); + return formattedCollection; }); } diff --git a/src/collections/operations/create.js b/src/collections/operations/create.js index 71c68431d..9d2d71f16 100644 --- a/src/collections/operations/create.js +++ b/src/collections/operations/create.js @@ -8,9 +8,9 @@ const getSafeFilename = require('../../uploads/getSafeFilename'); const getImageSize = require('../../uploads/getImageSize'); const imageMIMETypes = require('../../uploads/imageMIMETypes'); -const performFieldOperations = require('../../fields/performFieldOperations'); +async function create(args) { + const { performFieldOperations } = this; -const create = async (args) => { const { collection: { Model, @@ -21,7 +21,7 @@ const create = async (args) => { locale, fallbackLocale, }, - config, + depth, } = args; let { data } = args; @@ -88,7 +88,7 @@ const create = async (args) => { // 4. Execute field-level access, hooks, and validation // ///////////////////////////////////// - data = await performFieldOperations(config, collectionConfig, { + data = await performFieldOperations(collectionConfig, { data, hook: 'beforeCreate', operationName: 'create', @@ -114,11 +114,12 @@ const create = async (args) => { // 6. Execute field-level hooks and access // ///////////////////////////////////// - result = await performFieldOperations(config, collectionConfig, { + result = await performFieldOperations(collectionConfig, { data: result, hook: 'afterRead', operationName: 'read', req, + depth, }); // ///////////////////////////////////// @@ -139,6 +140,6 @@ const create = async (args) => { // ///////////////////////////////////// return result; -}; +} module.exports = create; diff --git a/src/collections/operations/delete.js b/src/collections/operations/delete.js index 83ca1db05..784e6964e 100644 --- a/src/collections/operations/delete.js +++ b/src/collections/operations/delete.js @@ -2,9 +2,7 @@ const fs = require('fs'); const { NotFound, Forbidden, ErrorDeletingFile } = require('../../errors'); const executeAccess = require('../../auth/executeAccess'); -const performFieldOperations = require('../../fields/performFieldOperations'); - -const deleteQuery = async (args) => { +async function deleteQuery(args) { const { depth, collection: { @@ -17,7 +15,6 @@ const deleteQuery = async (args) => { locale, fallbackLocale, }, - config, } = args; // ///////////////////////////////////// @@ -97,7 +94,7 @@ const deleteQuery = async (args) => { // 6. Execute field-level hooks and access // ///////////////////////////////////// - result = await performFieldOperations(config, collectionConfig, { + result = await this.performFieldOperations(collectionConfig, { data: result, hook: 'afterRead', operationName: 'read', @@ -120,6 +117,6 @@ const deleteQuery = async (args) => { // ///////////////////////////////////// return result; -}; +} module.exports = deleteQuery; diff --git a/src/collections/operations/find.js b/src/collections/operations/find.js index 519fcfcc0..236847ec1 100644 --- a/src/collections/operations/find.js +++ b/src/collections/operations/find.js @@ -1,12 +1,10 @@ const executeAccess = require('../../auth/executeAccess'); -const performFieldOperations = require('../../fields/performFieldOperations'); -const find = async (args) => { +async function find(args) { const { where, page, limit, - config, depth, collection: { Model, @@ -90,13 +88,17 @@ const find = async (args) => { const data = doc.toJSON({ virtuals: true }); - return performFieldOperations(config, collectionConfig, { - depth, - data, - req, - hook: 'afterRead', - operationName: 'read', - }); + return this.performFieldOperations( + collectionConfig, + { + depth, + data, + req, + hook: 'afterRead', + operationName: 'read', + }, + find, + ); })), }; @@ -128,6 +130,6 @@ const find = async (args) => { // ///////////////////////////////////// return afterReadResult; -}; +} module.exports = find; diff --git a/src/collections/operations/findByID.js b/src/collections/operations/findByID.js index d8e5aa70e..c7c106431 100644 --- a/src/collections/operations/findByID.js +++ b/src/collections/operations/findByID.js @@ -1,10 +1,8 @@ const { Forbidden, NotFound } = require('../../errors'); const executeAccess = require('../../auth/executeAccess'); -const performFieldOperations = require('../../fields/performFieldOperations'); -const findByID = async (args) => { +async function findByID(args) { const { - config, depth, collection: { Model, @@ -16,13 +14,15 @@ const findByID = async (args) => { locale, fallbackLocale, }, + disableErrors, + currentDepth, } = args; // ///////////////////////////////////// // 1. Retrieve and execute access // ///////////////////////////////////// - const accessResults = await executeAccess({ req }, collectionConfig.access.read); + const accessResults = await executeAccess({ req, disableErrors }, collectionConfig.access.read); const hasWhereAccess = typeof accessResults === 'object'; const queryToBuild = { @@ -55,8 +55,14 @@ const findByID = async (args) => { let result = await Model.findOne(query, {}); - if (!result && !hasWhereAccess) throw new NotFound(); - if (!result && hasWhereAccess) throw new Forbidden(); + if (!result) { + if (!disableErrors) { + if (!hasWhereAccess) throw new NotFound(); + if (hasWhereAccess) throw new Forbidden(); + } + + return null; + } if (locale && result.setLocale) { result.setLocale(locale, fallbackLocale); @@ -68,12 +74,13 @@ const findByID = async (args) => { // 4. Execute field-level hooks and access // ///////////////////////////////////// - result = await performFieldOperations(config, collectionConfig, { + result = await this.performFieldOperations(collectionConfig, { depth, req, data: result, hook: 'afterRead', operationName: 'read', + currentDepth, }); @@ -96,6 +103,6 @@ const findByID = async (args) => { // ///////////////////////////////////// return result; -}; +} module.exports = findByID; diff --git a/src/collections/operations/index.js b/src/collections/operations/index.js deleted file mode 100644 index 6ec58ad38..000000000 --- a/src/collections/operations/index.js +++ /dev/null @@ -1,13 +0,0 @@ -const findByID = require('./findByID'); -const find = require('./find'); -const create = require('./create'); -const update = require('./update'); -const deleteQuery = require('./delete'); - -module.exports = { - findByID, - find, - create, - update, - deleteQuery, -}; diff --git a/src/collections/operations/update.js b/src/collections/operations/update.js index aeab18284..15fbfc0fd 100644 --- a/src/collections/operations/update.js +++ b/src/collections/operations/update.js @@ -2,16 +2,14 @@ const deepmerge = require('deepmerge'); const overwriteMerge = require('../../utilities/overwriteMerge'); const executeAccess = require('../../auth/executeAccess'); const { NotFound, Forbidden } = require('../../errors'); -const performFieldOperations = require('../../fields/performFieldOperations'); const imageMIMETypes = require('../../uploads/imageMIMETypes'); const getImageSize = require('../../uploads/getImageSize'); const getSafeFilename = require('../../uploads/getSafeFilename'); const resizeAndSave = require('../../uploads/imageResizer'); -const update = async (args) => { +async function update(args) { const { - config, depth, collection: { Model, @@ -91,7 +89,7 @@ const update = async (args) => { // 4. Execute field-level hooks, access, and validation // ///////////////////////////////////// - data = await performFieldOperations(config, collectionConfig, { + data = await this.performFieldOperations(collectionConfig, { data, req, originalDoc, @@ -154,7 +152,7 @@ const update = async (args) => { // 7. Execute field-level hooks and access // ///////////////////////////////////// - doc = await performFieldOperations(config, collectionConfig, { + doc = await this.performFieldOperations(collectionConfig, { data: doc, hook: 'afterRead', operationName: 'read', @@ -180,6 +178,6 @@ const update = async (args) => { // ///////////////////////////////////// return doc; -}; +} module.exports = update; diff --git a/src/collections/requestHandlers/create.js b/src/collections/requestHandlers/create.js index 06b2ce449..f9f41341d 100644 --- a/src/collections/requestHandlers/create.js +++ b/src/collections/requestHandlers/create.js @@ -1,13 +1,11 @@ const httpStatus = require('http-status'); const formatSuccessResponse = require('../../express/responses/formatSuccess'); -const { create } = require('../operations'); -const createHandler = (config) => async (req, res, next) => { +async function create(req, res, next) { try { - const doc = await create({ + const doc = await this.operations.collections.create({ req, collection: req.collection, - config, data: req.body, depth: req.query.depth, }); @@ -19,6 +17,6 @@ const createHandler = (config) => async (req, res, next) => { } catch (error) { return next(error); } -}; +} -module.exports = createHandler; +module.exports = create; diff --git a/src/collections/requestHandlers/delete.js b/src/collections/requestHandlers/delete.js index 30a7230de..40e83d34a 100644 --- a/src/collections/requestHandlers/delete.js +++ b/src/collections/requestHandlers/delete.js @@ -1,13 +1,11 @@ const httpStatus = require('http-status'); const { NotFound } = require('../../errors'); -const { deleteQuery } = require('../operations'); -const deleteHandler = (config) => async (req, res, next) => { +async function deleteHandler(req, res, next) { try { - const doc = await deleteQuery({ + const doc = await this.operations.collections.deleteQuery({ req, collection: req.collection, - config, id: req.params.id, depth: req.query.depth, }); @@ -20,6 +18,6 @@ const deleteHandler = (config) => async (req, res, next) => { } catch (error) { return next(error); } -}; +} module.exports = deleteHandler; diff --git a/src/collections/requestHandlers/find.js b/src/collections/requestHandlers/find.js index 904a44c90..322cdfd68 100644 --- a/src/collections/requestHandlers/find.js +++ b/src/collections/requestHandlers/find.js @@ -1,12 +1,10 @@ const httpStatus = require('http-status'); -const { find } = require('../operations'); -const findHandler = (config) => async (req, res, next) => { +async function find(req, res, next) { try { const options = { req, collection: req.collection, - config, where: req.query.where, page: req.query.page, limit: req.query.limit, @@ -14,12 +12,12 @@ const findHandler = (config) => async (req, res, next) => { depth: req.query.depth, }; - const result = await find(options); + const result = await this.operations.collections.find(options); return res.status(httpStatus.OK).json(result); } catch (error) { return next(error); } -}; +} -module.exports = findHandler; +module.exports = find; diff --git a/src/collections/requestHandlers/findByID.js b/src/collections/requestHandlers/findByID.js index 35b9eaeba..b7004c137 100644 --- a/src/collections/requestHandlers/findByID.js +++ b/src/collections/requestHandlers/findByID.js @@ -1,20 +1,17 @@ -const { findByID } = require('../operations'); - -const findByIDHandler = (config) => async (req, res, next) => { +async function findByID(req, res, next) { const options = { req, collection: req.collection, - config, id: req.params.id, depth: req.query.depth, }; try { - const doc = await findByID(options); + const doc = await this.operations.collections.findByID(options); return res.json(doc); } catch (error) { return next(error); } -}; +} -module.exports = findByIDHandler; +module.exports = findByID; diff --git a/src/collections/requestHandlers/index.js b/src/collections/requestHandlers/index.js deleted file mode 100644 index 8422458dd..000000000 --- a/src/collections/requestHandlers/index.js +++ /dev/null @@ -1,13 +0,0 @@ -const create = require('./create'); -const deleteHandler = require('./delete'); -const findByID = require('./findByID'); -const find = require('./find'); -const update = require('./update'); - -module.exports = { - create, - deleteHandler, - findByID, - find, - update, -}; diff --git a/src/collections/requestHandlers/update.js b/src/collections/requestHandlers/update.js index bf81a199e..59c8a3692 100644 --- a/src/collections/requestHandlers/update.js +++ b/src/collections/requestHandlers/update.js @@ -1,13 +1,11 @@ const httpStatus = require('http-status'); const formatSuccessResponse = require('../../express/responses/formatSuccess'); -const { update } = require('../operations'); -const updateHandler = (config) => async (req, res, next) => { +async function update(req, res, next) { try { - const doc = await update({ + const doc = await this.operations.collections.update({ req, collection: req.collection, - config, id: req.params.id, data: req.body, depth: req.query.depth, @@ -20,6 +18,6 @@ const updateHandler = (config) => async (req, res, next) => { } catch (error) { return next(error); } -}; +} -module.exports = updateHandler; +module.exports = update; diff --git a/src/collections/routes.js b/src/collections/routes.js deleted file mode 100644 index 7a721410a..000000000 --- a/src/collections/routes.js +++ /dev/null @@ -1,27 +0,0 @@ -const express = require('express'); - -const requestHandlers = require('./requestHandlers'); -const bindCollectionMiddleware = require('./bindCollection'); - -const { - find, create, findByID, deleteHandler, update, -} = requestHandlers; - -const router = express.Router(); - -const registerRoutes = (collection, config) => { - router.all(`/${collection.config.slug}*`, bindCollectionMiddleware(collection)); - - router.route(`/${collection.config.slug}`) - .get(find(config)) - .post(create(config)); - - router.route(`/${collection.config.slug}/:id`) - .get(findByID(config)) - .put(update(config)) - .delete(deleteHandler(config)); - - return router; -}; - -module.exports = registerRoutes; diff --git a/src/fields/performFieldOperations.js b/src/fields/performFieldOperations.js index 6fec7a0e0..a076e8995 100644 --- a/src/fields/performFieldOperations.js +++ b/src/fields/performFieldOperations.js @@ -1,6 +1,7 @@ const { ValidationError } = require('../errors'); +const executeAccess = require('../auth/executeAccess'); -const performFieldOperations = async (config, entityConfig, operation) => { +async function performFieldOperations(entityConfig, operation) { const { data: fullData, originalDoc: fullOriginalDoc, @@ -9,12 +10,54 @@ const performFieldOperations = async (config, entityConfig, operation) => { req, } = operation; + const recursivePerformFieldOperations = performFieldOperations.bind(this); + + const depth = (operation.depth || operation.depth === 0) ? parseInt(operation.depth, 10) : this.config.defaultDepth; + const currentDepth = operation.currentDepth || 0; + + const populateRelationship = async (dataReference, data, field, i) => { + let dataToUpdate = dataReference; + + const relation = Array.isArray(field.relationTo) ? data.relationTo : field.relationTo; + const relatedCollection = this.collections[relation]; + + const accessResult = await executeAccess({ req, disableErrors: true }, relatedCollection.config.access.read); + + let populatedRelationship = null; + + if (accessResult && (depth && currentDepth <= depth)) { + populatedRelationship = await this.operations.collections.findByID({ + req, + collection: relatedCollection, + id: Array.isArray(field.relationTo) ? data.value : data, + currentDepth: currentDepth + 1, + disableErrors: true, + }); + } + + // If access control fails, update value to null + // If populatedRelationship comes back, update value + if (!accessResult || populatedRelationship) { + if (typeof i === 'number') { + if (Array.isArray(field.relationTo)) { + dataToUpdate[field.name][i].value = populatedRelationship; + } else { + dataToUpdate[field.name][i] = populatedRelationship; + } + } else if (Array.isArray(field.relationTo)) { + dataToUpdate.value = populatedRelationship; + } else { + dataToUpdate = populatedRelationship; + } + } + }; + // Maintain a top-level list of promises // so that all async field access / validations / hooks // can run in parallel const validationPromises = []; const accessPromises = []; - const relationshipAccessPromises = []; + const relationshipPopulationPromises = []; const hookPromises = []; const errors = []; @@ -36,13 +79,25 @@ const performFieldOperations = async (config, entityConfig, operation) => { } }; - const createRelationshipAccessPromise = async (data, field, access) => { + const createRelationshipPopulationPromise = async (data, field) => { const resultingData = data; - const result = await access({ req }); + if (field.hasMany && Array.isArray(data[field.name])) { + const rowPromises = []; - if (result === false) { - delete resultingData[field.name]; + data[field.name].forEach((relatedDoc, i) => { + const rowPromise = async () => { + if (relatedDoc) { + await populateRelationship(resultingData, relatedDoc, field, i); + } + }; + + rowPromises.push(rowPromise()); + }); + + await Promise.all(rowPromises); + } else if (data[field.name]) { + await populateRelationship(resultingData, data[field.name], field); } }; @@ -59,30 +114,13 @@ const performFieldOperations = async (config, entityConfig, operation) => { } } - if (field.type === 'relationship' && operationName === 'read') { - const relatedCollections = Array.isArray(field.relationTo) ? field.relationTo : [field.relationTo]; - - relatedCollections.forEach((slug) => { - const collection = config.collections.find((coll) => coll.slug === slug); - - if (collection && collection.access && collection.access.read) { - relationshipAccessPromises.push(createRelationshipAccessPromise(data, field, collection.access.read)); - } - }); + if ((field.type === 'relationship' || field.type === 'upload') && hook === 'afterRead') { + relationshipPopulationPromises.push(createRelationshipPopulationPromise(data, field)); } }; const createHookPromise = async (data, field) => { const resultingData = data; - const findRelatedCollection = (relation) => config.collections.find((collection) => collection.slug === relation); - // Todo: - // Check for afterRead operation and if found, - // Run relationship and upload-based hooks here - // Handle following scenarios: - // - // hasMany - // relationTo hasMany - // single if (hook === 'afterRead') { if ((field.type === 'relationship' || field.type === 'upload')) { @@ -90,51 +128,58 @@ const performFieldOperations = async (config, entityConfig, operation) => { // If there are many related documents if (field.hasMany && Array.isArray(data[field.name])) { + const relationshipDocPromises = []; // Loop through relations - data[field.name].forEach(async (value, i) => { - let relation = field.relationTo; + data[field.name].forEach((value, i) => { + const generateRelationshipDocPromise = async () => { + let relation = field.relationTo; - // If this field can be related to many collections, - // Set relationTo based on value - if (hasManyRelations && value && value.relationTo) { - relation = value.relationTo; - } + // If this field can be related to many collections, + // Set relationTo based on value + if (hasManyRelations && value && value.relationTo) { + relation = value.relationTo; + } - if (relation) { - const relatedCollection = findRelatedCollection(relation); + if (relation) { + const relatedCollection = this.collections[relation].config; - if (relatedCollection) { - let relatedDocumentData = data[field.name][i]; - let dataToHook = resultingData[field.name][i]; + if (relatedCollection) { + let relatedDocumentData = data[field.name][i]; + let dataToHook = resultingData[field.name][i]; - if (hasManyRelations) { - relatedDocumentData = data[field.name][i].value; - dataToHook = resultingData[field.name][i].value; - } + if (hasManyRelations) { + relatedDocumentData = data[field.name][i].value; + dataToHook = resultingData[field.name][i].value; + } - // Only run hooks for populated sub documents - NOT IDs - if (relatedDocumentData && typeof relatedDocumentData !== 'string') { - // Perform field hooks on related collection - dataToHook = await performFieldOperations(config, relatedCollection, { - req, - data: relatedDocumentData, - hook: 'afterRead', - operationName: 'read', - }); - - await relatedCollection.hooks.afterRead.reduce(async (priorHook, currentHook) => { - await priorHook; - - dataToHook = await currentHook({ + // Only run hooks for populated sub documents - NOT IDs + if (relatedDocumentData && typeof relatedDocumentData !== 'string') { + // Perform field hooks on related collection + dataToHook = await recursivePerformFieldOperations(relatedCollection, { req, - doc: relatedDocumentData, - }) || dataToHook; - }, Promise.resolve()); + data: relatedDocumentData, + hook: 'afterRead', + operationName: 'read', + }); + + await relatedCollection.hooks.afterRead.reduce(async (priorHook, currentHook) => { + await priorHook; + + dataToHook = await currentHook({ + req, + doc: relatedDocumentData, + }) || dataToHook; + }, Promise.resolve()); + } } } - } + }; + + relationshipDocPromises.push(generateRelationshipDocPromise()); }); + await Promise.all(relationshipDocPromises); + // Otherwise, there is only one related document } else { let relation = field.relationTo; @@ -143,7 +188,7 @@ const performFieldOperations = async (config, entityConfig, operation) => { relation = data[field.name].relationTo; } - const relatedCollection = findRelatedCollection(relation); + const relatedCollection = this.collections[relation].config; if (relatedCollection) { let relatedDocumentData = data[field.name]; @@ -157,7 +202,7 @@ const performFieldOperations = async (config, entityConfig, operation) => { // Only run hooks for populated sub documents - NOT IDs if (relatedDocumentData && typeof relatedDocumentData !== 'string') { // Perform field hooks on related collection - dataToHook = await performFieldOperations(config, relatedCollection, { + dataToHook = await recursivePerformFieldOperations(relatedCollection, { req, data: relatedDocumentData, hook: 'afterRead', @@ -255,10 +300,11 @@ const performFieldOperations = async (config, entityConfig, operation) => { } await Promise.all(accessPromises); + await Promise.all(relationshipPopulationPromises); await Promise.all(hookPromises); return fullData; -}; +} module.exports = performFieldOperations; diff --git a/src/globals/init.js b/src/globals/init.js index 24333a386..ef49baa5b 100644 --- a/src/globals/init.js +++ b/src/globals/init.js @@ -1,5 +1,5 @@ +const express = require('express'); const buildModel = require('./buildModel'); -const routes = require('./routes'); function initGlobals() { if (this.config.globals) { @@ -8,7 +8,16 @@ function initGlobals() { config: this.config.globals, }; - this.router.use(routes(this.config, this.globals.Model)); + const router = express.Router(); + + this.config.globals.forEach((global) => { + router + .route(`/globals/${global.slug}`) + .get(this.requestHandlers.globals.findOne(global)) + .post(this.requestHandlers.globals.update(global)); + }); + + this.router.use(router); } } diff --git a/src/globals/operations/findOne.js b/src/globals/operations/findOne.js index 9c0272714..bd8f9f622 100644 --- a/src/globals/operations/findOne.js +++ b/src/globals/operations/findOne.js @@ -1,11 +1,10 @@ const executeAccess = require('../../auth/executeAccess'); -const performFieldOperations = require('../../fields/performFieldOperations'); -const findOne = async (args) => { +async function findOne(args) { + const { globals: { Model } } = this; + const { - config, globalConfig, - Model, req, req: { locale, @@ -48,7 +47,7 @@ const findOne = async (args) => { // 4. Execute field-level hooks and access // ///////////////////////////////////// - doc = performFieldOperations(config, globalConfig, { + doc = this.performFieldOperations(globalConfig, { data: doc, hook: 'afterRead', operationName: 'read', @@ -74,6 +73,6 @@ const findOne = async (args) => { // ///////////////////////////////////// return doc; -}; +} module.exports = findOne; diff --git a/src/globals/operations/index.js b/src/globals/operations/index.js deleted file mode 100644 index 3a7c971df..000000000 --- a/src/globals/operations/index.js +++ /dev/null @@ -1,7 +0,0 @@ -const update = require('./update'); -const findOne = require('./findOne'); - -module.exports = { - findOne, - update, -}; diff --git a/src/globals/operations/update.js b/src/globals/operations/update.js index 1c2e2486c..0e0f5a74e 100644 --- a/src/globals/operations/update.js +++ b/src/globals/operations/update.js @@ -1,13 +1,12 @@ const deepmerge = require('deepmerge'); const overwriteMerge = require('../../utilities/overwriteMerge'); const executeAccess = require('../../auth/executeAccess'); -const performFieldOperations = require('../../fields/performFieldOperations'); -const update = async (args) => { +async function update(args) { + const { config, globals: { Model } } = this; + const { - config, globalConfig, - Model, slug, req, req: { @@ -65,7 +64,7 @@ const update = async (args) => { // 5. Execute field-level hooks, access, and validation // ///////////////////////////////////// - data = await performFieldOperations(config, globalConfig, { + data = await this.performFieldOperations(globalConfig, { data, req, hook: 'beforeUpdate', @@ -87,7 +86,7 @@ const update = async (args) => { // 7. Execute field-level hooks and access // ///////////////////////////////////// - global = await performFieldOperations(config, globalConfig, { + global = await this.performFieldOperations(globalConfig, { data: global, hook: 'afterRead', operationName: 'read', @@ -113,6 +112,6 @@ const update = async (args) => { // ///////////////////////////////////// return global; -}; +} module.exports = update; diff --git a/src/globals/requestHandlers/findOne.js b/src/globals/requestHandlers/findOne.js index b923db615..0178b1dd1 100644 --- a/src/globals/requestHandlers/findOne.js +++ b/src/globals/requestHandlers/findOne.js @@ -1,23 +1,26 @@ const httpStatus = require('http-status'); -const { findOne } = require('../operations'); -const findOneHandler = (config, Model, globalConfig) => async (req, res, next) => { - try { - const { slug } = globalConfig; +function findOne(globalConfig) { + async function handler(req, res, next) { + try { + const { slug } = globalConfig; - const result = await findOne({ - req, - Model, - config, - globalConfig, - slug, - depth: req.query.depth, - }); + const result = await this.operations.globals.findOne({ + req, + globalConfig, + slug, + depth: req.query.depth, + }); - return res.status(httpStatus.OK).json(result); - } catch (error) { - return next(error); + return res.status(httpStatus.OK).json(result); + } catch (error) { + return next(error); + } } -}; -module.exports = findOneHandler; + const findOneHandler = handler.bind(this); + + return findOneHandler; +} + +module.exports = findOne; diff --git a/src/globals/requestHandlers/index.js b/src/globals/requestHandlers/index.js deleted file mode 100644 index 511bbcb22..000000000 --- a/src/globals/requestHandlers/index.js +++ /dev/null @@ -1,7 +0,0 @@ -const findOne = require('./findOne'); -const update = require('./update'); - -module.exports = { - findOne, - update, -}; diff --git a/src/globals/requestHandlers/update.js b/src/globals/requestHandlers/update.js index d2b4020eb..80e131dc3 100644 --- a/src/globals/requestHandlers/update.js +++ b/src/globals/requestHandlers/update.js @@ -1,24 +1,26 @@ const httpStatus = require('http-status'); -const { update } = require('../operations'); -const updateHandler = (config, Model, globalConfig) => async (req, res, next) => { - try { - const { slug } = globalConfig; +function update(globalConfig) { + async function handler(req, res, next) { + try { + const { slug } = globalConfig; - const result = await update({ - req, - Model, - config, - globalConfig, - slug, - depth: req.query.depth, - data: req.body, - }); + const result = await this.operations.globals.update({ + req, + globalConfig, + slug, + depth: req.query.depth, + data: req.body, + }); - return res.status(httpStatus.OK).json({ message: 'Global saved successfully.', result }); - } catch (error) { - return next(error); + return res.status(httpStatus.OK).json({ message: 'Global saved successfully.', result }); + } catch (error) { + return next(error); + } } -}; -module.exports = updateHandler; + const updateHandler = handler.bind(this); + return updateHandler; +} + +module.exports = update; diff --git a/src/globals/routes.js b/src/globals/routes.js deleted file mode 100644 index 0ab285d4d..000000000 --- a/src/globals/routes.js +++ /dev/null @@ -1,19 +0,0 @@ -const express = require('express'); -const requestHandlers = require('./requestHandlers'); - -const { update, findOne } = requestHandlers; - -const router = express.Router(); - -const registerGlobals = (config, Globals) => { - config.globals.forEach((global) => { - router - .route(`/globals/${global.slug}`) - .get(findOne(config, Globals, global)) - .post(update(config, Globals, global)); - }); - - return router; -}; - -module.exports = registerGlobals; diff --git a/src/graphql/schema/buildObjectType.js b/src/graphql/schema/buildObjectType.js index c4e1f7635..a37efcd27 100644 --- a/src/graphql/schema/buildObjectType.js +++ b/src/graphql/schema/buildObjectType.js @@ -16,7 +16,6 @@ const { GraphQLJSON } = require('graphql-type-json'); const formatName = require('../utilities/formatName'); const combineParentName = require('../utilities/combineParentName'); const withNullableType = require('./withNullableType'); -const { find } = require('../../collections/operations'); function buildObjectType(name, fields, parentName, baseFields = {}) { const recursiveBuildObjectType = buildObjectType.bind(this); @@ -122,7 +121,7 @@ function buildObjectType(name, fields, parentName, baseFields = {}) { id = relatedDoc.value; } - const result = await find({ + const result = await this.operations.collections.find({ Model: this.collections[relatedCollectionSlug].Model, query: { where: { @@ -169,7 +168,7 @@ function buildObjectType(name, fields, parentName, baseFields = {}) { if (args.page) relatedDocumentQuery.paginate.page = args.page; if (args.limit) relatedDocumentQuery.paginate.limit = args.limit; - const relatedDocument = await find(); + const relatedDocument = await this.operations.collections.find(); if (relatedDocument.docs[0]) return relatedDocument.docs[0]; diff --git a/src/index.js b/src/index.js index 473f0f8b1..9758dc3cf 100644 --- a/src/index.js +++ b/src/index.js @@ -2,7 +2,9 @@ require('es6-promise').polyfill(); require('isomorphic-fetch'); const express = require('express'); -const graphQLPlayground = require('graphql-playground-middleware-express').default; +// const graphQLPlayground = require('graphql-playground-middleware-express').default; +const bindOperations = require('./init/bindOperations'); +const bindRequestHandlers = require('./init/bindRequestHandlers'); const getConfig = require('./utilities/getConfig'); const authenticate = require('./express/middleware/authenticate'); const connectMongoose = require('./mongoose/connect'); @@ -12,12 +14,12 @@ const initAuth = require('./auth/init'); const initCollections = require('./collections/init'); const initGlobals = require('./globals/init'); const initStatic = require('./express/static'); -const GraphQL = require('./graphql'); +// const GraphQL = require('./graphql'); const sanitizeConfig = require('./utilities/sanitizeConfig'); const buildEmail = require('./email/build'); -const identifyAPI = require('./express/middleware/identifyAPI'); +// const identifyAPI = require('./express/middleware/identifyAPI'); const errorHandler = require('./express/middleware/errorHandler'); -const { access } = require('./auth/requestHandlers'); +const performFieldOperations = require('./fields/performFieldOperations'); class Payload { constructor(options) { @@ -31,6 +33,9 @@ class Payload { this.router = express.Router(); this.collections = {}; + bindOperations(this); + bindRequestHandlers(this); + this.initAuth = initAuth.bind(this); this.initCollections = initCollections.bind(this); this.initGlobals = initGlobals.bind(this); @@ -39,6 +44,7 @@ class Payload { this.getMockEmailCredentials = this.getMockEmailCredentials.bind(this); this.initStatic = initStatic.bind(this); this.initAdmin = initAdmin.bind(this); + this.performFieldOperations = performFieldOperations.bind(this); // Configure email service this.email = this.buildEmail(); @@ -53,22 +59,22 @@ class Payload { this.initGlobals(); this.initAdmin(); - this.router.get('/access', access(this.config)); + this.router.get('/access', this.requestHandlers.collections.auth.access); - const graphQLHandler = new GraphQL(this); + // const graphQLHandler = new GraphQL(this); - this.router.use( - this.config.routes.graphQL, - identifyAPI('GraphQL'), - (req, res) => graphQLHandler.init(req, res)(req, res), - ); + // this.router.use( + // this.config.routes.graphQL, + // identifyAPI('GraphQL'), + // (req, res) => graphQLHandler.init(req, res)(req, res), + // ); - this.router.get(this.config.routes.graphQLPlayground, graphQLPlayground({ - endpoint: `${this.config.routes.api}${this.config.routes.graphQL}`, - settings: { - 'request.credentials': 'include', - }, - })); + // this.router.get(this.config.routes.graphQLPlayground, graphQLPlayground({ + // endpoint: `${this.config.routes.api}${this.config.routes.graphQL}`, + // settings: { + // 'request.credentials': 'include', + // }, + // })); // Bind router to API this.express.use(this.config.routes.api, this.router); diff --git a/src/init/bindOperations.js b/src/init/bindOperations.js new file mode 100644 index 000000000..07a273668 --- /dev/null +++ b/src/init/bindOperations.js @@ -0,0 +1,53 @@ +const access = require('../auth/operations/access'); +const forgotPassword = require('../auth/operations/forgotPassword'); +const init = require('../auth/operations/init'); +const login = require('../auth/operations/login'); +const logout = require('../auth/operations/logout'); +const me = require('../auth/operations/me'); +const refresh = require('../auth/operations/refresh'); +const register = require('../auth/operations/register'); +const registerFirstUser = require('../auth/operations/registerFirstUser'); +const resetPassword = require('../auth/operations/resetPassword'); +const authUpdate = require('../auth/operations/update'); + +const create = require('../collections/operations/create'); +const find = require('../collections/operations/find'); +const findByID = require('../collections/operations/findByID'); +const update = require('../collections/operations/update'); +const deleteHandler = require('../collections/operations/delete'); + +const findOne = require('../globals/operations/findOne'); +const globalUpdate = require('../globals/operations/update'); + +function bindOperations(ctx) { + const payload = ctx; + + payload.operations = { + collections: { + create: create.bind(ctx), + find: find.bind(ctx), + findByID: findByID.bind(ctx), + update: update.bind(ctx), + delete: deleteHandler.bind(ctx), + auth: { + access: access.bind(ctx), + forgotPassword: forgotPassword.bind(ctx), + init: init.bind(ctx), + login: login.bind(ctx), + logout: logout.bind(ctx), + me: me.bind(ctx), + refresh: refresh.bind(ctx), + register: register.bind(ctx), + registerFirstUser: registerFirstUser.bind(ctx), + resetPassword: resetPassword.bind(ctx), + update: authUpdate.bind(ctx), + }, + }, + globals: { + findOne: findOne.bind(ctx), + update: globalUpdate.bind(ctx), + }, + }; +} + +module.exports = bindOperations; diff --git a/src/init/bindRequestHandlers.js b/src/init/bindRequestHandlers.js new file mode 100644 index 000000000..a9dbb4fa8 --- /dev/null +++ b/src/init/bindRequestHandlers.js @@ -0,0 +1,53 @@ +const access = require('../auth/requestHandlers/access'); +const forgotPassword = require('../auth/requestHandlers/forgotPassword'); +const init = require('../auth/requestHandlers/init'); +const login = require('../auth/requestHandlers/login'); +const logout = require('../auth/requestHandlers/logout'); +const me = require('../auth/requestHandlers/me'); +const refresh = require('../auth/requestHandlers/refresh'); +const register = require('../auth/requestHandlers/register'); +const registerFirstUser = require('../auth/requestHandlers/registerFirstUser'); +const resetPassword = require('../auth/requestHandlers/resetPassword'); +const authUpdate = require('../auth/requestHandlers/update'); + +const create = require('../collections/requestHandlers/create'); +const find = require('../collections/requestHandlers/find'); +const findByID = require('../collections/requestHandlers/findByID'); +const update = require('../collections/requestHandlers/update'); +const deleteHandler = require('../collections/requestHandlers/delete'); + +const findOne = require('../globals/requestHandlers/findOne'); +const globalUpdate = require('../globals/requestHandlers/update'); + +function bindRequestHandlers(ctx) { + const payload = ctx; + + payload.requestHandlers = { + collections: { + create: create.bind(ctx), + find: find.bind(ctx), + findByID: findByID.bind(ctx), + update: update.bind(ctx), + delete: deleteHandler.bind(ctx), + auth: { + access: access.bind(ctx), + forgotPassword: forgotPassword.bind(ctx), + init: init.bind(ctx), + login: login.bind(ctx), + logout: logout.bind(ctx), + me: me.bind(ctx), + refresh: refresh.bind(ctx), + register: register.bind(ctx), + registerFirstUser: registerFirstUser.bind(ctx), + resetPassword: resetPassword.bind(ctx), + update: authUpdate.bind(ctx), + }, + }, + globals: { + findOne: findOne.bind(ctx), + update: globalUpdate.bind(ctx), + }, + }; +} + +module.exports = bindRequestHandlers; From 365803efe6dcc27e266982344e00ffb032d5ca40 Mon Sep 17 00:00:00 2001 From: James Date: Mon, 20 Jul 2020 16:19:20 -0400 Subject: [PATCH 096/125] fixes bug where many relationTo relationships were populating incorrectly --- src/fields/performFieldOperations.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/fields/performFieldOperations.js b/src/fields/performFieldOperations.js index a076e8995..babca22df 100644 --- a/src/fields/performFieldOperations.js +++ b/src/fields/performFieldOperations.js @@ -16,7 +16,7 @@ async function performFieldOperations(entityConfig, operation) { const currentDepth = operation.currentDepth || 0; const populateRelationship = async (dataReference, data, field, i) => { - let dataToUpdate = dataReference; + const dataToUpdate = dataReference; const relation = Array.isArray(field.relationTo) ? data.relationTo : field.relationTo; const relatedCollection = this.collections[relation]; @@ -45,9 +45,9 @@ async function performFieldOperations(entityConfig, operation) { dataToUpdate[field.name][i] = populatedRelationship; } } else if (Array.isArray(field.relationTo)) { - dataToUpdate.value = populatedRelationship; + dataToUpdate[field.name].value = populatedRelationship; } else { - dataToUpdate = populatedRelationship; + dataToUpdate[field.name] = populatedRelationship; } } }; @@ -188,9 +188,8 @@ async function performFieldOperations(entityConfig, operation) { relation = data[field.name].relationTo; } - const relatedCollection = this.collections[relation].config; - - if (relatedCollection) { + if (typeof relation === 'string') { + const relatedCollection = this.collections[relation].config; let relatedDocumentData = data[field.name]; let dataToHook = resultingData[field.name]; From 5771b3e5614c90b0d68266d2c52c993635ecffd5 Mon Sep 17 00:00:00 2001 From: James Date: Mon, 20 Jul 2020 17:14:31 -0400 Subject: [PATCH 097/125] updates resolvers to new architecture --- src/auth/graphql/resolvers/access.js | 14 ++--- src/auth/graphql/resolvers/forgotPassword.js | 28 ++++----- src/auth/graphql/resolvers/index.js | 23 ------- src/auth/graphql/resolvers/init.js | 24 ++++---- src/auth/graphql/resolvers/login.js | 35 +++++------ src/auth/graphql/resolvers/logout.js | 27 ++++---- src/auth/graphql/resolvers/me.js | 8 +-- src/auth/graphql/resolvers/refresh.js | 34 ++++++----- src/auth/graphql/resolvers/register.js | 46 +++++++------- src/auth/graphql/resolvers/resetPassword.js | 32 +++++----- src/auth/graphql/resolvers/update.js | 35 ++++++----- src/collections/graphql/init.js | 61 +++++++++++-------- src/collections/graphql/resolvers/create.js | 35 ++++++----- src/collections/graphql/resolvers/delete.js | 29 +++++---- src/collections/graphql/resolvers/find.js | 36 +++++------ src/collections/graphql/resolvers/findByID.js | 30 +++++---- src/collections/graphql/resolvers/index.js | 13 ---- src/collections/graphql/resolvers/update.js | 35 ++++++----- src/fields/performFieldOperations.js | 10 ++- src/globals/graphql/init.js | 12 ++-- src/globals/graphql/resolvers/findOne.js | 36 +++++------ src/globals/graphql/resolvers/update.js | 37 +++++------ src/graphql/index.js | 4 +- src/index.js | 32 +++++----- src/init/bindResolvers.js | 53 ++++++++++++++++ 25 files changed, 397 insertions(+), 332 deletions(-) delete mode 100644 src/auth/graphql/resolvers/index.js delete mode 100644 src/collections/graphql/resolvers/index.js create mode 100644 src/init/bindResolvers.js diff --git a/src/auth/graphql/resolvers/access.js b/src/auth/graphql/resolvers/access.js index da4be370c..a2adcdc5c 100644 --- a/src/auth/graphql/resolvers/access.js +++ b/src/auth/graphql/resolvers/access.js @@ -1,4 +1,3 @@ -const { access } = require('../../operations'); const formatName = require('../../../graphql/utilities/formatName'); const formatConfigNames = (results, configs) => { @@ -13,18 +12,17 @@ const formatConfigNames = (results, configs) => { return formattedResults; }; -const accessResolver = (config) => async (_, __, context) => { +async function access(_, __, context) { const options = { - config, req: context.req, }; - let accessResults = await access(options); + let accessResults = await this.operations.collections.auth.access(options); - accessResults = formatConfigNames(accessResults, config.collections); - accessResults = formatConfigNames(accessResults, config.globals); + accessResults = formatConfigNames(accessResults, this.config.collections); + accessResults = formatConfigNames(accessResults, this.config.globals); return accessResults; -}; +} -module.exports = accessResolver; +module.exports = access; diff --git a/src/auth/graphql/resolvers/forgotPassword.js b/src/auth/graphql/resolvers/forgotPassword.js index b874bafd7..6fdbc3845 100644 --- a/src/auth/graphql/resolvers/forgotPassword.js +++ b/src/auth/graphql/resolvers/forgotPassword.js @@ -1,18 +1,18 @@ -/* eslint-disable no-param-reassign */ -const { forgotPassword } = require('../../operations'); +function forgotPassword(collection) { + async function resolver(_, args, context) { + const options = { + collection, + data: args, + req: context.req, + }; -const forgotPasswordResolver = (config, collection, email) => async (_, args, context) => { - const options = { - config, - collection, - data: args, - email, - req: context.req, - }; + await this.operations.collections.auth.forgotPassword(options); + return true; + } - await forgotPassword(options); + const forgotPasswordResolver = resolver.bind(this); - return true; -}; + return forgotPasswordResolver; +} -module.exports = forgotPasswordResolver; +module.exports = forgotPassword; diff --git a/src/auth/graphql/resolvers/index.js b/src/auth/graphql/resolvers/index.js deleted file mode 100644 index 9347af057..000000000 --- a/src/auth/graphql/resolvers/index.js +++ /dev/null @@ -1,23 +0,0 @@ -const login = require('./login'); -const me = require('./me'); -const refresh = require('./refresh'); -const register = require('./register'); -const init = require('./init'); -const forgotPassword = require('./forgotPassword'); -const resetPassword = require('./resetPassword'); -const update = require('./update'); -const access = require('./access'); -const logout = require('./logout'); - -module.exports = { - login, - me, - refresh, - init, - register, - forgotPassword, - resetPassword, - update, - access, - logout, -}; diff --git a/src/auth/graphql/resolvers/init.js b/src/auth/graphql/resolvers/init.js index 9a49564f8..6d7b3fa8a 100644 --- a/src/auth/graphql/resolvers/init.js +++ b/src/auth/graphql/resolvers/init.js @@ -1,15 +1,17 @@ -/* eslint-disable no-param-reassign */ -const { init } = require('../../operations'); +function init({ Model }) { + async function resolver(_, __, context) { + const options = { + Model, + req: context.req, + }; -const initResolver = ({ Model }) => async (_, __, context) => { - const options = { - Model, - req: context.req, - }; + const result = await this.operations.collections.auth.init(options); - const result = await init(options); + return result; + } - return result; -}; + const initResolver = resolver.bind(this); + return initResolver; +} -module.exports = initResolver; +module.exports = init; diff --git a/src/auth/graphql/resolvers/login.js b/src/auth/graphql/resolvers/login.js index ad61ba4be..664bf4644 100644 --- a/src/auth/graphql/resolvers/login.js +++ b/src/auth/graphql/resolvers/login.js @@ -1,21 +1,22 @@ -/* eslint-disable no-param-reassign */ -const { login } = require('../../operations'); +function login(collection) { + async function resolver(_, args, context) { + const options = { + collection, + data: { + email: args.email, + password: args.password, + }, + req: context.req, + res: context.res, + }; -const loginResolver = (config, collection) => async (_, args, context) => { - const options = { - collection, - config, - data: { - email: args.email, - password: args.password, - }, - req: context.req, - res: context.res, - }; + const token = await this.operations.collections.auth.login(options); - const token = await login(options); + return token; + } - return token; -}; + const loginResolver = resolver.bind(this); + return loginResolver; +} -module.exports = loginResolver; +module.exports = login; diff --git a/src/auth/graphql/resolvers/logout.js b/src/auth/graphql/resolvers/logout.js index ec2263593..54c3abf47 100644 --- a/src/auth/graphql/resolvers/logout.js +++ b/src/auth/graphql/resolvers/logout.js @@ -1,17 +1,18 @@ -/* eslint-disable no-param-reassign */ -const { logout } = require('../../operations'); +function logout(collection) { + async function resolver(_, __, context) { + const options = { + collection, + res: context.res, + req: context.req, + }; -const logoutResolver = (config, collection) => async (_, __, context) => { - const options = { - config, - collection, - res: context.res, - req: context.req, - }; + const result = await this.operations.collections.auth.logout(options); - const result = await logout(options); + return result; + } - return result; -}; + const logoutResolver = resolver.bind(this); + return logoutResolver; +} -module.exports = logoutResolver; +module.exports = logout; diff --git a/src/auth/graphql/resolvers/me.js b/src/auth/graphql/resolvers/me.js index da09ce897..80eb12812 100644 --- a/src/auth/graphql/resolvers/me.js +++ b/src/auth/graphql/resolvers/me.js @@ -1,5 +1,5 @@ -const { me } = require('../../operations'); +async function me(_, __, context) { + return this.operations.collections.auth.me({ req: context.req }); +} -const meResolver = (config) => async (_, __, context) => me({ req: context.req, config }); - -module.exports = meResolver; +module.exports = me; diff --git a/src/auth/graphql/resolvers/refresh.js b/src/auth/graphql/resolvers/refresh.js index b382fc899..95a6abd6d 100644 --- a/src/auth/graphql/resolvers/refresh.js +++ b/src/auth/graphql/resolvers/refresh.js @@ -1,22 +1,24 @@ -/* eslint-disable no-param-reassign */ -const { refresh } = require('../../operations'); const getExtractJWT = require('../../getExtractJWT'); -const refreshResolver = (config, collection) => async (_, __, context) => { - const extractJWT = getExtractJWT(config); - const token = extractJWT(context); +function refresh(collection) { + async function resolver(_, __, context) { + const extractJWT = getExtractJWT(this.config); + const token = extractJWT(context); - const options = { - config, - collection, - token, - req: context.req, - res: context.res, - }; + const options = { + collection, + token, + req: context.req, + res: context.res, + }; - const result = await refresh(options); + const result = await this.operations.collections.auth.refresh(options); - return result; -}; + return result; + } -module.exports = refreshResolver; + const refreshResolver = resolver.bind(this); + return refreshResolver; +} + +module.exports = refresh; diff --git a/src/auth/graphql/resolvers/register.js b/src/auth/graphql/resolvers/register.js index 8a601e91a..b1d9f2031 100644 --- a/src/auth/graphql/resolvers/register.js +++ b/src/auth/graphql/resolvers/register.js @@ -1,28 +1,30 @@ /* eslint-disable no-param-reassign */ -const { register } = require('../../operations'); +function register(collection) { + async function resolver(_, args, context) { + const options = { + collection, + data: args.data, + depth: 0, + req: context.req, + }; -const registerResolver = (config, collection) => async (_, args, context) => { - const options = { - config, - collection, - data: args.data, - depth: 0, - req: context.req, - }; + if (args.locale) { + context.req.locale = args.locale; + options.locale = args.locale; + } - if (args.locale) { - context.req.locale = args.locale; - options.locale = args.locale; + if (args.fallbackLocale) { + context.req.fallbackLocale = args.fallbackLocale; + options.fallbackLocale = args.fallbackLocale; + } + + const token = await this.operations.collections.auth.register(options); + + return token; } - if (args.fallbackLocale) { - context.req.fallbackLocale = args.fallbackLocale; - options.fallbackLocale = args.fallbackLocale; - } + const registerResolver = resolver.bind(this); + return registerResolver; +} - const token = await register(options); - - return token; -}; - -module.exports = registerResolver; +module.exports = register; diff --git a/src/auth/graphql/resolvers/resetPassword.js b/src/auth/graphql/resolvers/resetPassword.js index 23e264a53..d57a2e8b2 100644 --- a/src/auth/graphql/resolvers/resetPassword.js +++ b/src/auth/graphql/resolvers/resetPassword.js @@ -1,21 +1,23 @@ /* eslint-disable no-param-reassign */ -const { resetPassword } = require('../../operations'); +function resetPassword(collection) { + async function resolver(_, args, context) { + if (args.locale) context.req.locale = args.locale; + if (args.fallbackLocale) context.req.fallbackLocale = args.fallbackLocale; -const resetPasswordResolver = (config, collection) => async (_, args, context) => { - if (args.locale) context.req.locale = args.locale; - if (args.fallbackLocale) context.req.fallbackLocale = args.fallbackLocale; + const options = { + collection, + data: args, + req: context.req, + api: 'GraphQL', + }; - const options = { - collection, - config, - data: args, - req: context.req, - api: 'GraphQL', - }; + const token = await this.operations.collections.auth.resetPassword(options); - const token = await resetPassword(options); + return token; + } - return token; -}; + const resetPasswordResolver = resolver.bind(this); + return resetPasswordResolver; +} -module.exports = resetPasswordResolver; +module.exports = resetPassword; diff --git a/src/auth/graphql/resolvers/update.js b/src/auth/graphql/resolvers/update.js index d19bef3c5..b7e0ca9ca 100644 --- a/src/auth/graphql/resolvers/update.js +++ b/src/auth/graphql/resolvers/update.js @@ -1,22 +1,25 @@ /* eslint-disable no-param-reassign */ -const { update } = require('../../operations'); -const updateResolver = (config, collection) => async (_, args, context) => { - if (args.locale) context.req.locale = args.locale; - if (args.fallbackLocale) context.req.fallbackLocale = args.fallbackLocale; +function update(collection) { + async function resolver(_, args, context) { + if (args.locale) context.req.locale = args.locale; + if (args.fallbackLocale) context.req.fallbackLocale = args.fallbackLocale; - const options = { - config, - collection, - data: args.data, - id: args.id, - depth: 0, - req: context.req, - }; + const options = { + collection, + data: args.data, + id: args.id, + depth: 0, + req: context.req, + }; - const user = await update(options); + const user = await this.operations.collections.auth.update(options); - return user; -}; + return user; + } -module.exports = updateResolver; + const updateResolver = resolver.bind(this); + return updateResolver; +} + +module.exports = update; diff --git a/src/collections/graphql/init.js b/src/collections/graphql/init.js index f5b0b5111..36e5a7477 100644 --- a/src/collections/graphql/init.js +++ b/src/collections/graphql/init.js @@ -9,17 +9,17 @@ const GraphQLDate = require('graphql-date'); const formatName = require('../../graphql/utilities/formatName'); -const { - create, find, findByID, deleteResolver, update, -} = require('./resolvers'); - -const { - login, logout, me, init, refresh, register, forgotPassword, resetPassword, -} = require('../../auth/graphql/resolvers'); - const buildPaginatedListType = require('../../graphql/schema/buildPaginatedListType'); function registerCollections() { + const { + create, find, findByID, deleteResolver, update, + } = this.graphQL.resolvers.collections; + + const { + login, logout, me, init, refresh, register, forgotPassword, resetPassword, update: authUpdate, + } = this.graphQL.resolvers.collections.auth; + Object.keys(this.collections).forEach((slug) => { const collection = this.collections[slug]; const { @@ -125,7 +125,7 @@ function registerCollections() { locale: { type: this.types.localeInputType }, fallbackLocale: { type: this.types.fallbackLocaleInputType }, }, - resolve: findByID(this.config, collection), + resolve: findByID(collection), }; this.Query.fields[pluralLabel] = { @@ -138,16 +138,7 @@ function registerCollections() { limit: { type: GraphQLInt }, sort: { type: GraphQLString }, }, - resolve: find(this.config, collection), - }; - - this.Mutation.fields[`update${singularLabel}`] = { - type: collection.graphQL.type, - args: { - id: { type: new GraphQLNonNull(GraphQLString) }, - data: { type: collection.graphQL.updateMutationInputType }, - }, - resolve: update(this.config, collection), + resolve: find(collection), }; this.Mutation.fields[`delete${singularLabel}`] = { @@ -190,7 +181,7 @@ function registerCollections() { }, }, }), - resolve: me(this.config), + resolve: me, }; this.Query.fields[`initialized${singularLabel}`] = { @@ -204,12 +195,12 @@ function registerCollections() { email: { type: GraphQLString }, password: { type: GraphQLString }, }, - resolve: login(this.config, collection), + resolve: login(collection), }; this.Mutation.fields[`logout${singularLabel}`] = { type: GraphQLString, - resolve: logout(this.config, collection), + resolve: logout(collection), }; this.Mutation.fields[`register${singularLabel}`] = { @@ -217,7 +208,7 @@ function registerCollections() { args: { data: { type: collection.graphQL.mutationInputType }, }, - resolve: register(this.config, collection), + resolve: register(collection), }; this.Mutation.fields[`forgotPassword${singularLabel}`] = { @@ -225,7 +216,7 @@ function registerCollections() { args: { email: { type: new GraphQLNonNull(GraphQLString) }, }, - resolve: forgotPassword(this.config, collection.Model, this.sendEmail), + resolve: forgotPassword(collection), }; this.Mutation.fields[`resetPassword${singularLabel}`] = { @@ -239,7 +230,16 @@ function registerCollections() { this.Mutation.fields[`refreshToken${singularLabel}`] = { type: GraphQLString, - resolve: refresh(this.config, collection), + resolve: refresh(collection), + }; + + this.Mutation.fields[`update${singularLabel}`] = { + type: collection.graphQL.type, + args: { + id: { type: new GraphQLNonNull(GraphQLString) }, + data: { type: collection.graphQL.updateMutationInputType }, + }, + resolve: authUpdate(collection), }; } else { this.Mutation.fields[`create${singularLabel}`] = { @@ -247,7 +247,16 @@ function registerCollections() { args: { data: { type: collection.graphQL.mutationInputType }, }, - resolve: create(this.config, collection), + resolve: create(collection), + }; + + this.Mutation.fields[`update${singularLabel}`] = { + type: collection.graphQL.type, + args: { + id: { type: new GraphQLNonNull(GraphQLString) }, + data: { type: collection.graphQL.updateMutationInputType }, + }, + resolve: update(collection), }; } }); diff --git a/src/collections/graphql/resolvers/create.js b/src/collections/graphql/resolvers/create.js index 25f0d3e08..659fa7c36 100644 --- a/src/collections/graphql/resolvers/create.js +++ b/src/collections/graphql/resolvers/create.js @@ -1,21 +1,24 @@ /* eslint-disable no-param-reassign */ -const { create } = require('../../operations'); -const createResolver = (config, collection) => async (_, args, context) => { - if (args.locale) { - context.req.locale = args.locale; +function create(collection) { + async function resolver(_, args, context) { + if (args.locale) { + context.req.locale = args.locale; + } + + const options = { + collection, + data: args.data, + req: context.req, + }; + + const result = await this.operations.collections.create(options); + + return result; } - const options = { - config, - collection, - data: args.data, - req: context.req, - }; + const createResolver = resolver.bind(this); + return createResolver; +} - const result = await create(options); - - return result; -}; - -module.exports = createResolver; +module.exports = create; diff --git a/src/collections/graphql/resolvers/delete.js b/src/collections/graphql/resolvers/delete.js index e5899716f..2aa35bc7a 100644 --- a/src/collections/graphql/resolvers/delete.js +++ b/src/collections/graphql/resolvers/delete.js @@ -1,19 +1,22 @@ /* eslint-disable no-param-reassign */ -const { deleteQuery } = require('../../operations'); +function getDeleteResolver(collection) { + async function resolver(_, args, context) { + if (args.locale) context.req.locale = args.locale; + if (args.fallbackLocale) context.req.fallbackLocale = args.fallbackLocale; -const deleteResolver = (collection) => async (_, args, context) => { - if (args.locale) context.req.locale = args.locale; - if (args.fallbackLocale) context.req.fallbackLocale = args.fallbackLocale; + const options = { + collection, + id: args.id, + req: context.req, + }; - const options = { - collection, - id: args.id, - req: context.req, - }; + const result = await this.operations.collections.delete(options); - const result = await deleteQuery(options); + return result; + } - return result; -}; + const deleteResolver = resolver.bind(this); + return deleteResolver; +} -module.exports = deleteResolver; +module.exports = getDeleteResolver; diff --git a/src/collections/graphql/resolvers/find.js b/src/collections/graphql/resolvers/find.js index 87d071f99..52833244e 100644 --- a/src/collections/graphql/resolvers/find.js +++ b/src/collections/graphql/resolvers/find.js @@ -1,22 +1,24 @@ /* eslint-disable no-param-reassign */ -const { find } = require('../../operations'); +function find(collection) { + async function resolver(_, args, context) { + if (args.locale) context.req.locale = args.locale; + if (args.fallbackLocale) context.req.fallbackLocale = args.fallbackLocale; -const findResolver = (config, collection) => async (_, args, context) => { - if (args.locale) context.req.locale = args.locale; - if (args.fallbackLocale) context.req.fallbackLocale = args.fallbackLocale; + const options = { + collection, + where: args.where, + limit: args.limit, + page: args.page, + sort: args.sort, + req: context.req, + }; - const options = { - config, - collection, - where: args.where, - limit: args.limit, - page: args.page, - sort: args.sort, - req: context.req, - }; + const results = await this.operations.collections.find(options); + return results; + } - const results = await find(options); - return results; -}; + const findResolver = resolver.bind(this); + return findResolver; +} -module.exports = findResolver; +module.exports = find; diff --git a/src/collections/graphql/resolvers/findByID.js b/src/collections/graphql/resolvers/findByID.js index 4bae467c0..7a06a0eeb 100644 --- a/src/collections/graphql/resolvers/findByID.js +++ b/src/collections/graphql/resolvers/findByID.js @@ -1,18 +1,22 @@ /* eslint-disable no-param-reassign */ -const findByIDResolver = (collection) => async (_, args, context) => { - if (args.locale) context.req.locale = args.locale; - if (args.fallbackLocale) context.req.fallbackLocale = args.fallbackLocale; +function findByID(collection) { + async function resolver(_, args, context) { + if (args.locale) context.req.locale = args.locale; + if (args.fallbackLocale) context.req.fallbackLocale = args.fallbackLocale; - const options = { - config, - collection, - id: args.id, - req: context.req, - }; + const options = { + collection, + id: args.id, + req: context.req, + }; - const result = await findByID(options); + const result = await this.operations.collections.findByID(options); - return result; -}; + return result; + } -module.exports = findByIDResolver; + const findByIDResolver = resolver.bind(this); + return findByIDResolver; +} + +module.exports = findByID; diff --git a/src/collections/graphql/resolvers/index.js b/src/collections/graphql/resolvers/index.js deleted file mode 100644 index 469eb71a8..000000000 --- a/src/collections/graphql/resolvers/index.js +++ /dev/null @@ -1,13 +0,0 @@ -const findByID = require('./findByID'); -const find = require('./find'); -const create = require('./create'); -const update = require('./update'); -const deleteResolver = require('./delete'); - -module.exports = { - findByID, - find, - create, - update, - deleteResolver, -}; diff --git a/src/collections/graphql/resolvers/update.js b/src/collections/graphql/resolvers/update.js index ec829889e..1a51d5050 100644 --- a/src/collections/graphql/resolvers/update.js +++ b/src/collections/graphql/resolvers/update.js @@ -1,22 +1,25 @@ /* eslint-disable no-param-reassign */ -const { update } = require('../../operations'); -const updateResolver = (config, collection) => async (_, args, context) => { - if (args.locale) context.req.locale = args.locale; - if (args.fallbackLocale) context.req.fallbackLocale = args.fallbackLocale; +function update(collection) { + async function resolver(_, args, context) { + if (args.locale) context.req.locale = args.locale; + if (args.fallbackLocale) context.req.fallbackLocale = args.fallbackLocale; - const options = { - config, - collection, - data: args.data, - id: args.id, - depth: 0, - req: context.req, - }; + const options = { + collection, + data: args.data, + id: args.id, + depth: 0, + req: context.req, + }; - const result = await update(options); + const result = await this.operations.collections.update(options); - return result; -}; + return result; + } -module.exports = updateResolver; + const updateResolver = resolver.bind(this); + return updateResolver; +} + +module.exports = update; diff --git a/src/fields/performFieldOperations.js b/src/fields/performFieldOperations.js index babca22df..0a83d99a9 100644 --- a/src/fields/performFieldOperations.js +++ b/src/fields/performFieldOperations.js @@ -8,11 +8,19 @@ async function performFieldOperations(entityConfig, operation) { operationName, hook, req, + req: { + payloadAPI, + }, } = operation; const recursivePerformFieldOperations = performFieldOperations.bind(this); - const depth = (operation.depth || operation.depth === 0) ? parseInt(operation.depth, 10) : this.config.defaultDepth; + let depth = 0; + + if (payloadAPI === 'REST') { + depth = (operation.depth || operation.depth === 0) ? parseInt(operation.depth, 10) : this.config.defaultDepth; + } + const currentDepth = operation.currentDepth || 0; const populateRelationship = async (dataReference, data, field, i) => { diff --git a/src/globals/graphql/init.js b/src/globals/graphql/init.js index 6e51dd5d4..1732c2a3a 100644 --- a/src/globals/graphql/init.js +++ b/src/globals/graphql/init.js @@ -1,12 +1,12 @@ const { GraphQLNonNull } = require('graphql'); const formatName = require('../../graphql/utilities/formatName'); -const { - findOne, update, -} = require('./resolvers'); - function registerGlobals() { if (this.config.globals) { + const { + findOne, update, + } = this.graphQL.resolvers.globals; + Object.keys(this.globals.config).forEach((slug) => { const global = this.globals.config[slug]; const { @@ -36,7 +36,7 @@ function registerGlobals() { locale: { type: this.types.localeInputType }, fallbackLocale: { type: this.types.fallbackLocaleInputType }, }, - resolve: findOne(this.config, this.globals.Model, global), + resolve: findOne(global), }; this.Mutation.fields[`update${formattedLabel}`] = { @@ -44,7 +44,7 @@ function registerGlobals() { args: { data: { type: global.graphQL.mutationInputType }, }, - resolve: update(this.config, this.globals.Model, global), + resolve: update(global), }; }); } diff --git a/src/globals/graphql/resolvers/findOne.js b/src/globals/graphql/resolvers/findOne.js index 9099c55f8..a2adc99d2 100644 --- a/src/globals/graphql/resolvers/findOne.js +++ b/src/globals/graphql/resolvers/findOne.js @@ -1,23 +1,25 @@ /* eslint-disable no-param-reassign */ -const { findOne } = require('../../operations'); -const findOneResolver = (config, Model, globalConfig) => async (_, args, context) => { - if (args.locale) context.req.locale = args.locale; - if (args.fallbackLocale) context.req.fallbackLocale = args.fallbackLocale; +function findOne(globalConfig) { + async function resolver(_, args, context) { + if (args.locale) context.req.locale = args.locale; + if (args.fallbackLocale) context.req.fallbackLocale = args.fallbackLocale; - const { slug } = globalConfig; + const { slug } = globalConfig; - const options = { - Model, - config, - globalConfig, - slug, - depth: 0, - req: context.req, - }; + const options = { + globalConfig, + slug, + depth: 0, + req: context.req, + }; - const result = await findOne(options); - return result; -}; + const result = await this.operations.globals.findOne(options); + return result; + } -module.exports = findOneResolver; + const findOneResolver = resolver.bind(this); + return findOneResolver; +} + +module.exports = findOne; diff --git a/src/globals/graphql/resolvers/update.js b/src/globals/graphql/resolvers/update.js index 00041c1a6..debde50a9 100644 --- a/src/globals/graphql/resolvers/update.js +++ b/src/globals/graphql/resolvers/update.js @@ -1,25 +1,26 @@ /* eslint-disable no-param-reassign */ -const { update } = require('../../operations'); -const updateResolver = (config, Model, globalConfig) => async (_, args, context) => { - if (args.locale) context.req.locale = args.locale; - if (args.fallbackLocale) context.req.fallbackLocale = args.fallbackLocale; +function update(globalConfig) { + async function resolver(_, args, context) { + if (args.locale) context.req.locale = args.locale; + if (args.fallbackLocale) context.req.fallbackLocale = args.fallbackLocale; - const { slug } = config; + const { slug } = globalConfig; - const options = { - config, - globalConfig, - Model, - data: args.data, - slug, - depth: 0, - req: context.req, - }; + const options = { + globalConfig, + slug, + depth: 0, + data: args.data, + req: context.req, + }; - const result = await update(options); + const result = await this.operations.globals.update(options); + return result; + } - return result; -}; + const findOneResolver = resolver.bind(this); + return findOneResolver; +} -module.exports = updateResolver; +module.exports = update; diff --git a/src/graphql/index.js b/src/graphql/index.js index 2a9d32498..249fb1677 100644 --- a/src/graphql/index.js +++ b/src/graphql/index.js @@ -11,7 +11,7 @@ const initCollections = require('../collections/graphql/init'); const initGlobals = require('../globals/graphql/init'); const buildWhereInputType = require('./schema/buildWhereInputType'); const errorHandler = require('./errorHandler'); -const { access } = require('../auth/graphql/resolvers'); +const access = require('../auth/graphql/resolvers/access'); class GraphQL { constructor(init) { @@ -50,7 +50,7 @@ class GraphQL { this.Query.fields.Access = { type: this.buildPoliciesType(), - resolve: access(this.config), + resolve: access, }; this.Query = { diff --git a/src/index.js b/src/index.js index 9758dc3cf..b1a3fc524 100644 --- a/src/index.js +++ b/src/index.js @@ -2,9 +2,10 @@ require('es6-promise').polyfill(); require('isomorphic-fetch'); const express = require('express'); -// const graphQLPlayground = require('graphql-playground-middleware-express').default; +const graphQLPlayground = require('graphql-playground-middleware-express').default; const bindOperations = require('./init/bindOperations'); const bindRequestHandlers = require('./init/bindRequestHandlers'); +const bindResolvers = require('./init/bindResolvers'); const getConfig = require('./utilities/getConfig'); const authenticate = require('./express/middleware/authenticate'); const connectMongoose = require('./mongoose/connect'); @@ -14,10 +15,10 @@ const initAuth = require('./auth/init'); const initCollections = require('./collections/init'); const initGlobals = require('./globals/init'); const initStatic = require('./express/static'); -// const GraphQL = require('./graphql'); +const GraphQL = require('./graphql'); const sanitizeConfig = require('./utilities/sanitizeConfig'); const buildEmail = require('./email/build'); -// const identifyAPI = require('./express/middleware/identifyAPI'); +const identifyAPI = require('./express/middleware/identifyAPI'); const errorHandler = require('./express/middleware/errorHandler'); const performFieldOperations = require('./fields/performFieldOperations'); @@ -35,6 +36,7 @@ class Payload { bindOperations(this); bindRequestHandlers(this); + bindResolvers(this); this.initAuth = initAuth.bind(this); this.initCollections = initCollections.bind(this); @@ -61,20 +63,20 @@ class Payload { this.router.get('/access', this.requestHandlers.collections.auth.access); - // const graphQLHandler = new GraphQL(this); + const graphQLHandler = new GraphQL(this); - // this.router.use( - // this.config.routes.graphQL, - // identifyAPI('GraphQL'), - // (req, res) => graphQLHandler.init(req, res)(req, res), - // ); + this.router.use( + this.config.routes.graphQL, + identifyAPI('GraphQL'), + (req, res) => graphQLHandler.init(req, res)(req, res), + ); - // this.router.get(this.config.routes.graphQLPlayground, graphQLPlayground({ - // endpoint: `${this.config.routes.api}${this.config.routes.graphQL}`, - // settings: { - // 'request.credentials': 'include', - // }, - // })); + this.router.get(this.config.routes.graphQLPlayground, graphQLPlayground({ + endpoint: `${this.config.routes.api}${this.config.routes.graphQL}`, + settings: { + 'request.credentials': 'include', + }, + })); // Bind router to API this.express.use(this.config.routes.api, this.router); diff --git a/src/init/bindResolvers.js b/src/init/bindResolvers.js new file mode 100644 index 000000000..a3f9176f7 --- /dev/null +++ b/src/init/bindResolvers.js @@ -0,0 +1,53 @@ +const access = require('../auth/graphql/resolvers/access'); +const forgotPassword = require('../auth/graphql/resolvers/forgotPassword'); +const init = require('../auth/graphql/resolvers/init'); +const login = require('../auth/graphql/resolvers/login'); +const logout = require('../auth/graphql/resolvers/logout'); +const me = require('../auth/graphql/resolvers/me'); +const refresh = require('../auth/graphql/resolvers/refresh'); +const register = require('../auth/graphql/resolvers/register'); +const resetPassword = require('../auth/graphql/resolvers/resetPassword'); +const authUpdate = require('../auth/graphql/resolvers/update'); + +const create = require('../collections/graphql/resolvers/create'); +const find = require('../collections/graphql/resolvers/find'); +const findByID = require('../collections/graphql/resolvers/findByID'); +const update = require('../collections/graphql/resolvers/update'); +const deleteResolver = require('../collections/graphql/resolvers/delete'); + +const findOne = require('../globals/graphql/resolvers/findOne'); +const globalUpdate = require('../globals/graphql/resolvers/update'); + +function bindResolvers(ctx) { + const payload = ctx; + + payload.graphQL = { + resolvers: { + collections: { + create: create.bind(ctx), + find: find.bind(ctx), + findByID: findByID.bind(ctx), + update: update.bind(ctx), + deleteResolver: deleteResolver.bind(ctx), + auth: { + access: access.bind(ctx), + forgotPassword: forgotPassword.bind(ctx), + init: init.bind(ctx), + login: login.bind(ctx), + logout: logout.bind(ctx), + me: me.bind(ctx), + refresh: refresh.bind(ctx), + register: register.bind(ctx), + resetPassword: resetPassword.bind(ctx), + update: authUpdate.bind(ctx), + }, + }, + globals: { + findOne: findOne.bind(ctx), + update: globalUpdate.bind(ctx), + }, + }, + }; +} + +module.exports = bindResolvers; From 9d784288d042748ebaafd7d7aac5565dad08b7dd Mon Sep 17 00:00:00 2001 From: James Date: Mon, 20 Jul 2020 17:43:01 -0400 Subject: [PATCH 098/125] ensures all tests pass --- src/auth/operations/forgotPassword.js | 2 +- src/auth/operations/registerFirstUser.js | 6 ++---- src/collections/init.js | 20 +++++++++++--------- src/collections/requestHandlers/delete.js | 2 +- src/fields/performFieldOperations.js | 3 ++- 5 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/auth/operations/forgotPassword.js b/src/auth/operations/forgotPassword.js index 3c287ec65..c8a112fe6 100644 --- a/src/auth/operations/forgotPassword.js +++ b/src/auth/operations/forgotPassword.js @@ -2,7 +2,7 @@ const crypto = require('crypto'); const { APIError } = require('../../errors'); async function forgotPassword(args) { - const { config, email } = this; + const { config, sendEmail: email } = this; if (!Object.prototype.hasOwnProperty.call(args.data, 'email')) { throw new APIError('Missing email.'); diff --git a/src/auth/operations/registerFirstUser.js b/src/auth/operations/registerFirstUser.js index dfa163a0d..57abfac9e 100644 --- a/src/auth/operations/registerFirstUser.js +++ b/src/auth/operations/registerFirstUser.js @@ -1,5 +1,3 @@ -const register = require('./register'); -const login = require('./login'); const { Forbidden } = require('../../errors'); async function registerFirstUser(args) { @@ -17,7 +15,7 @@ async function registerFirstUser(args) { // 2. Perform register first user // ///////////////////////////////////// - let result = await register({ + let result = await this.operations.collections.auth.register({ ...args, overrideAccess: true, }); @@ -27,7 +25,7 @@ async function registerFirstUser(args) { // 3. Log in new user // ///////////////////////////////////// - const token = await login({ + const token = await this.operations.collections.auth.login({ ...args, }); diff --git a/src/collections/init.js b/src/collections/init.js index 8d3b92f0b..3f82b6d9c 100644 --- a/src/collections/init.js +++ b/src/collections/init.js @@ -43,14 +43,6 @@ function registerCollections() { delete: deleteHandler, } = this.requestHandlers.collections; - router.route(`/${slug}`) - .get(find) - .put(update); - - router.route(`/${slug}/:id`) - .get(findByID) - .delete(deleteHandler); - if (collection.auth) { const AuthCollection = this.collections[formattedCollection.slug]; passport.use(new LocalStrategy(AuthCollection.Model.authenticate())); @@ -108,12 +100,22 @@ function registerCollections() { .route(`/${slug}/register`) .post(register); + router.route(`/${slug}`) + .get(find); + router.route(`/${slug}/:id`) - .put(authUpdate); + .get(findByID) + .put(authUpdate) + .delete(deleteHandler); } else { router.route(`/${slug}`) .get(find) .post(create); + + router.route(`/${slug}/:id`) + .put(update) + .get(findByID) + .delete(deleteHandler); } this.router.use(router); diff --git a/src/collections/requestHandlers/delete.js b/src/collections/requestHandlers/delete.js index 40e83d34a..4f44aac49 100644 --- a/src/collections/requestHandlers/delete.js +++ b/src/collections/requestHandlers/delete.js @@ -3,7 +3,7 @@ const { NotFound } = require('../../errors'); async function deleteHandler(req, res, next) { try { - const doc = await this.operations.collections.deleteQuery({ + const doc = await this.operations.collections.delete({ req, collection: req.collection, id: req.params.id, diff --git a/src/fields/performFieldOperations.js b/src/fields/performFieldOperations.js index 0a83d99a9..ee2e9ba6f 100644 --- a/src/fields/performFieldOperations.js +++ b/src/fields/performFieldOperations.js @@ -21,7 +21,7 @@ async function performFieldOperations(entityConfig, operation) { depth = (operation.depth || operation.depth === 0) ? parseInt(operation.depth, 10) : this.config.defaultDepth; } - const currentDepth = operation.currentDepth || 0; + const currentDepth = operation.currentDepth || 1; const populateRelationship = async (dataReference, data, field, i) => { const dataToUpdate = dataReference; @@ -40,6 +40,7 @@ async function performFieldOperations(entityConfig, operation) { id: Array.isArray(field.relationTo) ? data.value : data, currentDepth: currentDepth + 1, disableErrors: true, + depth, }); } From fa5fffee7216a9a2c5027204fdf2828f43bbe8d1 Mon Sep 17 00:00:00 2001 From: James Date: Mon, 20 Jul 2020 17:44:08 -0400 Subject: [PATCH 099/125] replaces strict access control in global --- demo/globals/GlobalWithStrictAccess.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/demo/globals/GlobalWithStrictAccess.js b/demo/globals/GlobalWithStrictAccess.js index 1361bfaae..ab15f5aa7 100644 --- a/demo/globals/GlobalWithStrictAccess.js +++ b/demo/globals/GlobalWithStrictAccess.js @@ -5,8 +5,7 @@ module.exports = { label: 'Global with Strict Access', access: { update: ({ req: { user } }) => checkRole(['admin'], user), - // read: ({ req: { user } }) => checkRole(['admin'], user), - read: () => true, + read: ({ req: { user } }) => checkRole(['admin'], user), }, fields: [ { From cfe75ccc9c12536571b0d4c51054af076d7fe74e Mon Sep 17 00:00:00 2001 From: James Date: Mon, 20 Jul 2020 17:44:24 -0400 Subject: [PATCH 100/125] 0.0.17 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0cb3ad825..faee40b85 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@payloadcms/payload", - "version": "0.0.16", + "version": "0.0.17", "description": "CMS and Application Framework", "license": "ISC", "author": "Payload CMS LLC", From 6737d089d1eb2bb30442335a0461f8a18347c64b Mon Sep 17 00:00:00 2001 From: James Date: Tue, 21 Jul 2020 20:56:26 -0400 Subject: [PATCH 101/125] ensures findByID returns 404 if no ID, passes more data to field hooks --- src/collections/operations/findByID.js | 2 ++ src/fields/performFieldOperations.js | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/collections/operations/findByID.js b/src/collections/operations/findByID.js index c7c106431..92bc080a5 100644 --- a/src/collections/operations/findByID.js +++ b/src/collections/operations/findByID.js @@ -53,6 +53,8 @@ async function findByID(args) { // 3. Perform database operation // ///////////////////////////////////// + if (!query.$and[0]._id) throw new NotFound(); + let result = await Model.findOne(query, {}); if (!result) { diff --git a/src/fields/performFieldOperations.js b/src/fields/performFieldOperations.js index ee2e9ba6f..16cae10fd 100644 --- a/src/fields/performFieldOperations.js +++ b/src/fields/performFieldOperations.js @@ -235,6 +235,8 @@ async function performFieldOperations(entityConfig, operation) { field.hooks[hook].forEach(async (fieldHook) => { resultingData[field.name] = await fieldHook({ value: data[field.name], + originalDoc: fullOriginalDoc, + data: fullData, req, }); }); From 4861d5469c9aa13e43ad6032f7013f9689794f99 Mon Sep 17 00:00:00 2001 From: James Date: Tue, 21 Jul 2020 21:05:12 -0400 Subject: [PATCH 102/125] clears history state on first load of client --- src/client/components/Routes.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/client/components/Routes.js b/src/client/components/Routes.js index c61a8f3e8..47f93353f 100644 --- a/src/client/components/Routes.js +++ b/src/client/components/Routes.js @@ -1,6 +1,6 @@ import React, { useState, useEffect } from 'react'; import { - Route, Switch, withRouter, Redirect, + Route, Switch, withRouter, Redirect, useHistory, } from 'react-router-dom'; import config from 'payload/config'; import List from './views/collections/List'; @@ -25,6 +25,7 @@ const { } = config; const Routes = () => { + const history = useHistory(); const [initialized, setInitialized] = useState(null); const { user, permissions, permissions: { canAccessAdmin } } = useUser(); @@ -36,6 +37,10 @@ const Routes = () => { })); }, []); + useEffect(() => { + history.replace(); + }, [history]); + return ( Date: Tue, 21 Jul 2020 21:06:00 -0400 Subject: [PATCH 103/125] 0.0.18 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index faee40b85..3732ad8a4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@payloadcms/payload", - "version": "0.0.17", + "version": "0.0.18", "description": "CMS and Application Framework", "license": "ISC", "author": "Payload CMS LLC", From ec181f0e74779861f98b15a978879e028da1393d Mon Sep 17 00:00:00 2001 From: James Date: Wed, 22 Jul 2020 08:39:14 -0400 Subject: [PATCH 104/125] utilizes find operation in jwt and api key strategies --- src/auth/init.js | 2 +- src/auth/operations/register.js | 2 -- src/auth/strategies/apiKey.js | 36 +++++++++++++++++++++--------- src/auth/strategies/jwt.js | 26 ++++++++++++++++----- src/collections/init.js | 2 +- src/collections/operations/find.js | 28 +++++++++++++---------- 6 files changed, 64 insertions(+), 32 deletions(-) diff --git a/src/auth/init.js b/src/auth/init.js index 5a49fc5aa..4bc4f841a 100644 --- a/src/auth/init.js +++ b/src/auth/init.js @@ -4,7 +4,7 @@ const jwtStrategy = require('./strategies/jwt'); function initAuth() { passport.use(new AnonymousStrategy.Strategy()); - passport.use('jwt', jwtStrategy(this.config, this.collections)); + passport.use('jwt', jwtStrategy(this)); } module.exports = initAuth; diff --git a/src/auth/operations/register.js b/src/auth/operations/register.js index 4bbc3054a..9dfcb1925 100644 --- a/src/auth/operations/register.js +++ b/src/auth/operations/register.js @@ -2,8 +2,6 @@ const passport = require('passport'); const executeAccess = require('../executeAccess'); async function register(args) { - const { config } = this; - const { depth, overrideAccess, diff --git a/src/auth/strategies/apiKey.js b/src/auth/strategies/apiKey.js index 89d83c22c..119d86455 100644 --- a/src/auth/strategies/apiKey.js +++ b/src/auth/strategies/apiKey.js @@ -1,20 +1,36 @@ const PassportAPIKey = require('passport-headerapikey').HeaderAPIKeyStrategy; -module.exports = ({ Model, config }) => { +module.exports = ({ operations }, { Model, config }) => { const opts = { header: 'Authorization', prefix: `${config.labels.singular} API-Key `, }; - return new PassportAPIKey(opts, false, (apiKey, done) => { - Model.findOne({ apiKey, enableAPIKey: true }, (err, user) => { - if (err) return done(err); - if (!user) return done(null, false); + return new PassportAPIKey(opts, true, async (req, apiKey, done) => { + try { + const userQuery = await operations.collections.find({ + where: { + apiKey: { + equals: apiKey, + }, + }, + collection: { + Model, + config, + }, + req, + overrideAccess: true, + }); - const json = user.toJSON({ virtuals: true }); - json.collection = config.slug; - - return done(null, json); - }); + if (userQuery.docs && userQuery.docs.length > 0) { + const user = userQuery.docs[0]; + user.collection = config.slug; + done(null, user); + } else { + done(null, false); + } + } catch (err) { + done(null, false); + } }); }; diff --git a/src/auth/strategies/jwt.js b/src/auth/strategies/jwt.js index 58eb9bacf..24cffc3cd 100644 --- a/src/auth/strategies/jwt.js +++ b/src/auth/strategies/jwt.js @@ -3,9 +3,10 @@ const getExtractJWT = require('../getExtractJWT'); const JwtStrategy = passportJwt.Strategy; -module.exports = (config, collections) => { +module.exports = ({ config, collections, operations }) => { const opts = { session: false, + passReqToCallback: true, }; const extractJWT = getExtractJWT(config); @@ -13,16 +14,29 @@ module.exports = (config, collections) => { opts.jwtFromRequest = extractJWT; opts.secretOrKey = config.secret; - return new JwtStrategy(opts, async (token, done) => { + return new JwtStrategy(opts, async (req, token, done) => { try { const collection = collections[token.collection]; - const user = await collection.Model.findByUsername(token.email); + const userQuery = await operations.collections.find({ + where: { + email: { + equals: token.email, + }, + }, + collection, + req, + overrideAccess: true, + }); - const json = user.toJSON({ virtuals: true }); - json.collection = collection.config.slug; + if (userQuery.docs && userQuery.docs.length > 0) { + const user = userQuery.docs[0]; + user.collection = collection.config.slug; - done(null, json); + done(null, user); + } else { + done(null, false); + } } catch (err) { done(null, false); } diff --git a/src/collections/init.js b/src/collections/init.js index 3f82b6d9c..7966e9f9f 100644 --- a/src/collections/init.js +++ b/src/collections/init.js @@ -48,7 +48,7 @@ function registerCollections() { passport.use(new LocalStrategy(AuthCollection.Model.authenticate())); if (collection.auth.useAPIKey) { - passport.use(`${AuthCollection.config.slug}-api-key`, apiKeyStrategy(AuthCollection)); + passport.use(`${AuthCollection.config.slug}-api-key`, apiKeyStrategy(this, AuthCollection)); } const { diff --git a/src/collections/operations/find.js b/src/collections/operations/find.js index 236847ec1..bf2613168 100644 --- a/src/collections/operations/find.js +++ b/src/collections/operations/find.js @@ -15,15 +15,13 @@ async function find(args) { locale, fallbackLocale, }, + overrideAccess, } = args; // ///////////////////////////////////// // 1. Retrieve and execute access // ///////////////////////////////////// - const accessResults = await executeAccess({ req }, collectionConfig.access.read); - const hasWhereAccess = typeof accessResults === 'object'; - const queryToBuild = {}; if (where) { @@ -32,15 +30,21 @@ async function find(args) { }; } - if (hasWhereAccess) { - if (!where) { - queryToBuild.where = { - and: [ - accessResults, - ], - }; - } else { - queryToBuild.where.and.push(accessResults); + if (!overrideAccess) { + const accessResults = await executeAccess({ req }, collectionConfig.access.read); + const hasWhereAccess = typeof accessResults === 'object'; + + + if (hasWhereAccess) { + if (!where) { + queryToBuild.where = { + and: [ + accessResults, + ], + }; + } else { + queryToBuild.where.and.push(accessResults); + } } } From a8c11e5b6c50d344b2f8bb13239e21e4e03e7e65 Mon Sep 17 00:00:00 2001 From: James Date: Wed, 22 Jul 2020 08:39:25 -0400 Subject: [PATCH 105/125] 0.0.19 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3732ad8a4..76daf9a31 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@payloadcms/payload", - "version": "0.0.18", + "version": "0.0.19", "description": "CMS and Application Framework", "license": "ISC", "author": "Payload CMS LLC", From 265d2b711888c909d754e9acb0c05753e9595762 Mon Sep 17 00:00:00 2001 From: James Date: Wed, 22 Jul 2020 08:51:02 -0400 Subject: [PATCH 106/125] properly skips access control in field operations if overrideAccess is set to true --- src/collections/operations/find.js | 1 + src/express/middleware/index.js | 52 +++++++++++++--------------- src/fields/performFieldOperations.js | 3 +- 3 files changed, 28 insertions(+), 28 deletions(-) diff --git a/src/collections/operations/find.js b/src/collections/operations/find.js index bf2613168..ba0c505fb 100644 --- a/src/collections/operations/find.js +++ b/src/collections/operations/find.js @@ -100,6 +100,7 @@ async function find(args) { req, hook: 'afterRead', operationName: 'read', + overrideAccess, }, find, ); diff --git a/src/express/middleware/index.js b/src/express/middleware/index.js index 2a883dfe3..fe6fdd571 100644 --- a/src/express/middleware/index.js +++ b/src/express/middleware/index.js @@ -9,34 +9,32 @@ const localizationMiddleware = require('../../localization/middleware'); const authenticate = require('./authenticate'); const identifyAPI = require('./identifyAPI'); -const middleware = (config) => { - return [ - passport.initialize(), - authenticate(config), - express.json(), - methodOverride('X-HTTP-Method-Override'), - qsMiddleware({ depth: 10 }), - bodyParser.urlencoded({ extended: true }), - compression(config.compression), - localizationMiddleware(config.localization), - identifyAPI('REST'), - fileUpload({ - parseNested: true, - }), - (req, res, next) => { - if (config.cors) { - if (config.cors.indexOf(req.headers.origin) > -1) { - res.setHeader('Access-Control-Allow-Origin', req.headers.origin); - res.header('Access-Control-Allow-Methods', 'PUT, POST, GET, DELETE, OPTIONS'); - } - - res.header('Access-Control-Allow-Headers', - 'Origin X-Requested-With, Content-Type, Accept, Authorization'); +const middleware = (config) => [ + passport.initialize(), + identifyAPI('REST'), + authenticate(config), + express.json(), + methodOverride('X-HTTP-Method-Override'), + qsMiddleware({ depth: 10 }), + bodyParser.urlencoded({ extended: true }), + compression(config.compression), + localizationMiddleware(config.localization), + fileUpload({ + parseNested: true, + }), + (req, res, next) => { + if (config.cors) { + if (config.cors.indexOf(req.headers.origin) > -1) { + res.setHeader('Access-Control-Allow-Origin', req.headers.origin); + res.header('Access-Control-Allow-Methods', 'PUT, POST, GET, DELETE, OPTIONS'); } - next(); - }, - ]; -}; + res.header('Access-Control-Allow-Headers', + 'Origin X-Requested-With, Content-Type, Accept, Authorization'); + } + + next(); + }, +]; module.exports = middleware; diff --git a/src/fields/performFieldOperations.js b/src/fields/performFieldOperations.js index 16cae10fd..44e78c049 100644 --- a/src/fields/performFieldOperations.js +++ b/src/fields/performFieldOperations.js @@ -11,6 +11,7 @@ async function performFieldOperations(entityConfig, operation) { req: { payloadAPI, }, + overrideAccess, } = operation; const recursivePerformFieldOperations = performFieldOperations.bind(this); @@ -29,7 +30,7 @@ async function performFieldOperations(entityConfig, operation) { const relation = Array.isArray(field.relationTo) ? data.relationTo : field.relationTo; const relatedCollection = this.collections[relation]; - const accessResult = await executeAccess({ req, disableErrors: true }, relatedCollection.config.access.read); + const accessResult = !overrideAccess ? await executeAccess({ req, disableErrors: true }, relatedCollection.config.access.read) : true; let populatedRelationship = null; From 38392d0b3659564b4d6599555b5308f3afb98e3b Mon Sep 17 00:00:00 2001 From: James Date: Wed, 22 Jul 2020 08:51:21 -0400 Subject: [PATCH 107/125] 0.0.20 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 76daf9a31..eef449bb6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@payloadcms/payload", - "version": "0.0.19", + "version": "0.0.20", "description": "CMS and Application Framework", "license": "ISC", "author": "Payload CMS LLC", From ae611c1dcf4b7827544a2f5f37bf382325d75908 Mon Sep 17 00:00:00 2001 From: James Date: Wed, 22 Jul 2020 09:10:37 -0400 Subject: [PATCH 108/125] ensures login sets JWT with properly formatted user --- src/auth/operations/login.js | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/src/auth/operations/login.js b/src/auth/operations/login.js index 8e6ac89a4..c3537f80d 100644 --- a/src/auth/operations/login.js +++ b/src/auth/operations/login.js @@ -2,7 +2,7 @@ const jwt = require('jsonwebtoken'); const { AuthenticationError } = require('../../errors'); async function login(args) { - const { config } = this; + const { config, operations } = this; const options = { ...args }; @@ -22,20 +22,38 @@ async function login(args) { config: collectionConfig, }, data, + req, } = options; const { email, password } = data; - const user = await Model.findByUsername(email); + const userDoc = await Model.findByUsername(email); - if (!user) throw new AuthenticationError(); - const authResult = await user.authenticate(password); + if (!userDoc) throw new AuthenticationError(); + + const authResult = await userDoc.authenticate(password); if (!authResult.user) { throw new AuthenticationError(); } + const userQuery = await operations.collections.find({ + where: { + email: { + equals: email, + }, + }, + collection: { + Model, + config: collectionConfig, + }, + req, + overrideAccess: true, + }); + + const user = userQuery.docs[0]; + const fieldsToSign = collectionConfig.fields.reduce((signedFields, field) => { if (field.saveToJWT) { return { From 06c0396259cfa0e739d8a0b56a0f2602b0dd56e3 Mon Sep 17 00:00:00 2001 From: James Date: Wed, 22 Jul 2020 09:15:38 -0400 Subject: [PATCH 109/125] 0.0.21 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index eef449bb6..df5a4d206 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@payloadcms/payload", - "version": "0.0.20", + "version": "0.0.21", "description": "CMS and Application Framework", "license": "ISC", "author": "Payload CMS LLC", From 94dd9904fcd2a5d2db2eccad115d5bfec98c6e03 Mon Sep 17 00:00:00 2001 From: James Date: Wed, 22 Jul 2020 14:55:48 -0400 Subject: [PATCH 110/125] fixes bug with afterRead hooks not firing in find operation --- src/collections/operations/find.js | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/src/collections/operations/find.js b/src/collections/operations/find.js index ba0c505fb..41b71f236 100644 --- a/src/collections/operations/find.js +++ b/src/collections/operations/find.js @@ -113,22 +113,20 @@ async function find(args) { let afterReadResult = result; - if (typeof afterRead === 'function') { - afterReadResult = { - ...result, - docs: await Promise.all(result.docs.map(async (doc) => { - let docRef = doc; + afterReadResult = { + ...result, + docs: await Promise.all(result.docs.map(async (doc) => { + let docRef = doc; - await collectionConfig.hooks.afterRead.reduce(async (priorHook, hook) => { - await priorHook; + await collectionConfig.hooks.afterRead.reduce(async (priorHook, hook) => { + await priorHook; - docRef = await hook({ req, query, doc }) || doc; - }, Promise.resolve()); + docRef = await hook({ req, query, doc }) || doc; + }, Promise.resolve()); - return docRef; - })), - }; - } + return docRef; + })), + }; // ///////////////////////////////////// // 7. Return results From cbb9b540c1140bac9a3e847126d21fbb2903383b Mon Sep 17 00:00:00 2001 From: James Date: Wed, 22 Jul 2020 15:22:45 -0400 Subject: [PATCH 111/125] 0.0.22 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index df5a4d206..b82da3a3d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@payloadcms/payload", - "version": "0.0.21", + "version": "0.0.22", "description": "CMS and Application Framework", "license": "ISC", "author": "Payload CMS LLC", From c1c7598da573788604346ed2740780bd3f8bca9c Mon Sep 17 00:00:00 2001 From: James Date: Wed, 22 Jul 2020 21:41:35 -0400 Subject: [PATCH 112/125] misc. bugfixes --- .../forms/DraggableSection/index.js | 128 +++++++++--------- .../forms/field-types/Array/index.scss | 4 - .../forms/field-types/Group/index.js | 10 +- .../views/collections/Edit/index.js | 2 +- .../views/collections/List/Default.js | 2 +- src/fields/performFieldOperations.js | 48 +++---- 6 files changed, 93 insertions(+), 101 deletions(-) diff --git a/src/client/components/forms/DraggableSection/index.js b/src/client/components/forms/DraggableSection/index.js index 06a46d67e..f71ce1ea2 100644 --- a/src/client/components/forms/DraggableSection/index.js +++ b/src/client/components/forms/DraggableSection/index.js @@ -48,79 +48,75 @@ const DraggableSection = (props) => { draggableId={id} index={rowIndex} > - {(providedDrag) => { - return ( -
    setIsHovered(false)} - onMouseOver={() => setIsHovered(true)} - onFocus={() => setIsHovered(true)} - {...providedDrag.draggableProps} - > + {(providedDrag) => ( +
    setIsHovered(false)} + onMouseOver={() => setIsHovered(true)} + onFocus={() => setIsHovered(true)} + {...providedDrag.draggableProps} + > -
    - +
    + -
    +
    - {blockType === 'blocks' && ( -
    - - -
    - )} - - - { - return ({ - ...field, - path: `${parentPath}.${rowIndex}${field.name ? `.${field.name}` : ''}`, - }); - })} + {blockType === 'blocks' && ( +
    + - -
    - +
    + )} + + + ({ + ...field, + path: `${parentPath}.${rowIndex}${field.name ? `.${field.name}` : ''}`, + }))} + /> +
    + +
    - ); - }} +
    + )} ); }; diff --git a/src/client/components/forms/field-types/Array/index.scss b/src/client/components/forms/field-types/Array/index.scss index 59a82bc0f..cd700bda9 100644 --- a/src/client/components/forms/field-types/Array/index.scss +++ b/src/client/components/forms/field-types/Array/index.scss @@ -33,9 +33,5 @@ .field-type.array__add-button-wrap { margin-left: base(2.65); } - - .field-type.array__header { - display: none; - } } } diff --git a/src/client/components/forms/field-types/Group/index.js b/src/client/components/forms/field-types/Group/index.js index 4e875ff5a..7e6c9fcd1 100644 --- a/src/client/components/forms/field-types/Group/index.js +++ b/src/client/components/forms/field-types/Group/index.js @@ -21,12 +21,10 @@ const Group = (props) => { initialData={initialData} fieldTypes={fieldTypes} customComponentsPath={`${customComponentsPath}${name}.fields.`} - fieldSchema={fields.map((subField) => { - return { - ...subField, - path: `${path}${subField.name ? `.${subField.name}` : ''}`, - }; - })} + fieldSchema={fields.map((subField) => ({ + ...subField, + path: `${path}${subField.name ? `.${subField.name}` : ''}`, + }))} />
    ); diff --git a/src/client/components/views/collections/Edit/index.js b/src/client/components/views/collections/Edit/index.js index 3b7e0f9e9..2802c3653 100644 --- a/src/client/components/views/collections/Edit/index.js +++ b/src/client/components/views/collections/Edit/index.js @@ -44,7 +44,7 @@ const EditView = (props) => { const [{ data, isLoading }] = usePayloadAPI( (isEditing ? `${serverURL}${api}/${slug}/${id}` : null), - { initialParams: { 'fallback-locale': 'null' } }, + { initialParams: { 'fallback-locale': 'null', depth: 0 } }, ); const dataToRender = locationState?.data || data; diff --git a/src/client/components/views/collections/List/Default.js b/src/client/components/views/collections/List/Default.js index c7411059f..c2eb2e2b4 100644 --- a/src/client/components/views/collections/List/Default.js +++ b/src/client/components/views/collections/List/Default.js @@ -44,7 +44,7 @@ const DefaultList = (props) => { const apiURL = `${serverURL}${api}/${slug}`; - const [{ data }, { setParams }] = usePayloadAPI(apiURL, {}); + const [{ data }, { setParams }] = usePayloadAPI(apiURL, { initialParams: { depth: 0 } }); useEffect(() => { const params = {}; diff --git a/src/fields/performFieldOperations.js b/src/fields/performFieldOperations.js index 44e78c049..e6c8a971b 100644 --- a/src/fields/performFieldOperations.js +++ b/src/fields/performFieldOperations.js @@ -30,34 +30,36 @@ async function performFieldOperations(entityConfig, operation) { const relation = Array.isArray(field.relationTo) ? data.relationTo : field.relationTo; const relatedCollection = this.collections[relation]; - const accessResult = !overrideAccess ? await executeAccess({ req, disableErrors: true }, relatedCollection.config.access.read) : true; + if (relatedCollection) { + const accessResult = !overrideAccess ? await executeAccess({ req, disableErrors: true }, relatedCollection.config.access.read) : true; - let populatedRelationship = null; + let populatedRelationship = null; - if (accessResult && (depth && currentDepth <= depth)) { - populatedRelationship = await this.operations.collections.findByID({ - req, - collection: relatedCollection, - id: Array.isArray(field.relationTo) ? data.value : data, - currentDepth: currentDepth + 1, - disableErrors: true, - depth, - }); - } + if (accessResult && (depth && currentDepth <= depth)) { + populatedRelationship = await this.operations.collections.findByID({ + req, + collection: relatedCollection, + id: Array.isArray(field.relationTo) ? data.value : data, + currentDepth: currentDepth + 1, + disableErrors: true, + depth, + }); + } - // If access control fails, update value to null - // If populatedRelationship comes back, update value - if (!accessResult || populatedRelationship) { - if (typeof i === 'number') { - if (Array.isArray(field.relationTo)) { - dataToUpdate[field.name][i].value = populatedRelationship; + // If access control fails, update value to null + // If populatedRelationship comes back, update value + if (!accessResult || populatedRelationship) { + if (typeof i === 'number') { + if (Array.isArray(field.relationTo)) { + dataToUpdate[field.name][i].value = populatedRelationship; + } else { + dataToUpdate[field.name][i] = populatedRelationship; + } + } else if (Array.isArray(field.relationTo)) { + dataToUpdate[field.name].value = populatedRelationship; } else { - dataToUpdate[field.name][i] = populatedRelationship; + dataToUpdate[field.name] = populatedRelationship; } - } else if (Array.isArray(field.relationTo)) { - dataToUpdate[field.name].value = populatedRelationship; - } else { - dataToUpdate[field.name] = populatedRelationship; } } }; From f42b2fc670718a64a60aca677672432666f6e797 Mon Sep 17 00:00:00 2001 From: James Date: Wed, 22 Jul 2020 22:49:35 -0400 Subject: [PATCH 113/125] 0.0.24 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b82da3a3d..458e2ffb3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@payloadcms/payload", - "version": "0.0.22", + "version": "0.0.24", "description": "CMS and Application Framework", "license": "ISC", "author": "Payload CMS LLC", From 28d8c10a3e87a380c3469b2759d3efe9b8eeca67 Mon Sep 17 00:00:00 2001 From: James Date: Thu, 23 Jul 2020 12:15:05 -0400 Subject: [PATCH 114/125] Revert "0.0.24" This reverts commit f42b2fc670718a64a60aca677672432666f6e797. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 458e2ffb3..b82da3a3d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@payloadcms/payload", - "version": "0.0.24", + "version": "0.0.22", "description": "CMS and Application Framework", "license": "ISC", "author": "Payload CMS LLC", From 8efc03b3253e41ae3e49532231882680f401c5fb Mon Sep 17 00:00:00 2001 From: James Date: Thu, 23 Jul 2020 12:18:13 -0400 Subject: [PATCH 115/125] 0.0.25 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b82da3a3d..d9461051f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@payloadcms/payload", - "version": "0.0.22", + "version": "0.0.25", "description": "CMS and Application Framework", "license": "ISC", "author": "Payload CMS LLC", From 846589e303f46442663e3b18f4a6b7d4b0939235 Mon Sep 17 00:00:00 2001 From: James Date: Thu, 23 Jul 2020 12:18:19 -0400 Subject: [PATCH 116/125] 0.0.26 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d9461051f..35e7f28e2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@payloadcms/payload", - "version": "0.0.25", + "version": "0.0.26", "description": "CMS and Application Framework", "license": "ISC", "author": "Payload CMS LLC", From d2055141bee51792c100bbf21896cb660e33268b Mon Sep 17 00:00:00 2001 From: James Date: Thu, 23 Jul 2020 17:40:16 -0400 Subject: [PATCH 117/125] performance enhancements and render reductions --- .../components/forms/RenderFields/index.js | 18 ++- .../forms/field-types/Array/index.js | 132 ++++++++++------ .../forms/field-types/Blocks/index.js | 144 +++++++++++------- 3 files changed, 195 insertions(+), 99 deletions(-) diff --git a/src/client/components/forms/RenderFields/index.js b/src/client/components/forms/RenderFields/index.js index ace2d7171..7f67c9156 100644 --- a/src/client/components/forms/RenderFields/index.js +++ b/src/client/components/forms/RenderFields/index.js @@ -1,4 +1,4 @@ -import React, { createContext, useContext } from 'react'; +import React, { createContext, useEffect, useContext, useState } from 'react'; import PropTypes from 'prop-types'; import RenderCustomComponent from '../../utilities/RenderCustomComponent'; @@ -22,12 +22,24 @@ const RenderFields = (props) => { const { customComponentsPath: customComponentsPathFromContext, operation: operationFromContext } = useRenderedFields(); - const customComponentsPath = customComponentsPathFromProps || customComponentsPathFromContext; const operation = operationFromProps || operationFromContext; + const customComponentsPath = customComponentsPathFromProps || customComponentsPathFromContext; + + const [contextValue, setContextValue] = useState({ + operation, + customComponentsPath, + }); + + useEffect(() => { + setContextValue({ + operation, + customComponentsPath, + }); + }, [operation, customComponentsPath]); if (fieldSchema) { return ( - + {fieldSchema.map((field, i) => { if (!field?.hidden && field?.admin?.disabled !== true) { if ((filter && typeof filter === 'function' && filter(field)) || !filter) { diff --git a/src/client/components/forms/field-types/Array/index.js b/src/client/components/forms/field-types/Array/index.js index 1c04c3f2b..f49b2ccb0 100644 --- a/src/client/components/forms/field-types/Array/index.js +++ b/src/client/components/forms/field-types/Array/index.js @@ -62,7 +62,7 @@ const ArrayFieldType = (props) => { required, }); - const addRow = (rowIndex) => { + const addRow = useCallback((rowIndex) => { const data = getDataByPath(path); dispatchRows({ @@ -70,9 +70,9 @@ const ArrayFieldType = (props) => { }); setValue(value + 1); - }; + }, [dispatchRows, getDataByPath, path, setValue, value]); - const removeRow = (rowIndex) => { + const removeRow = useCallback((rowIndex) => { const data = getDataByPath(path); dispatchRows({ @@ -82,22 +82,22 @@ const ArrayFieldType = (props) => { }); setValue(value - 1); - }; + }, [dispatchRows, path, getDataByPath, setValue, value]); - const moveRow = (moveFromIndex, moveToIndex) => { + const moveRow = useCallback((moveFromIndex, moveToIndex) => { const data = getDataByPath(path); dispatchRows({ type: 'MOVE', index: moveFromIndex, moveToIndex, data, }); - }; + }, [dispatchRows, getDataByPath, path]); - const onDragEnd = (result) => { + const onDragEnd = useCallback((result) => { if (!result.destination) return; const sourceIndex = result.source.index; const destinationIndex = result.destination.index; moveRow(sourceIndex, destinationIndex); - }; + }, [moveRow]); useEffect(() => { dispatchRows({ @@ -123,6 +123,84 @@ const ArrayFieldType = (props) => { } }, [value, setValue, disableFormData, dataToInitialize]); + return ( + + ); +}; + +ArrayFieldType.defaultProps = { + label: '', + defaultValue: [], + initialData: [], + validate: array, + required: false, + maxRows: undefined, + minRows: undefined, + singularLabel: 'Row', + permissions: {}, +}; + +ArrayFieldType.propTypes = { + defaultValue: PropTypes.arrayOf( + PropTypes.shape({}), + ), + initialData: PropTypes.arrayOf( + PropTypes.shape({}), + ), + fields: PropTypes.arrayOf( + PropTypes.shape({}), + ).isRequired, + label: PropTypes.string, + singularLabel: PropTypes.string, + name: PropTypes.string.isRequired, + path: PropTypes.string.isRequired, + fieldTypes: PropTypes.shape({}).isRequired, + validate: PropTypes.func, + required: PropTypes.bool, + maxRows: PropTypes.number, + minRows: PropTypes.number, + permissions: PropTypes.shape({ + fields: PropTypes.shape({}), + }), +}; + +const RenderArray = React.memo((props) => { + const { + onDragEnd, + label, + showError, + errorMessage, + rows, + singularLabel, + addRow, + removeRow, + moveRow, + path, + customComponentsPath, + name, + fieldTypes, + fields, + permissions, + value, + } = props; + return (
    @@ -179,42 +257,6 @@ const ArrayFieldType = (props) => {
    ); -}; - -ArrayFieldType.defaultProps = { - label: '', - defaultValue: [], - initialData: [], - validate: array, - required: false, - maxRows: undefined, - minRows: undefined, - singularLabel: 'Row', - permissions: {}, -}; - -ArrayFieldType.propTypes = { - defaultValue: PropTypes.arrayOf( - PropTypes.shape({}), - ), - initialData: PropTypes.arrayOf( - PropTypes.shape({}), - ), - fields: PropTypes.arrayOf( - PropTypes.shape({}), - ).isRequired, - label: PropTypes.string, - singularLabel: PropTypes.string, - name: PropTypes.string.isRequired, - path: PropTypes.string.isRequired, - fieldTypes: PropTypes.shape({}).isRequired, - validate: PropTypes.func, - required: PropTypes.bool, - maxRows: PropTypes.number, - minRows: PropTypes.number, - permissions: PropTypes.shape({ - fields: PropTypes.shape({}), - }), -}; +}); export default withCondition(ArrayFieldType); diff --git a/src/client/components/forms/field-types/Blocks/index.js b/src/client/components/forms/field-types/Blocks/index.js index 3f9d631f6..716ab4902 100644 --- a/src/client/components/forms/field-types/Blocks/index.js +++ b/src/client/components/forms/field-types/Blocks/index.js @@ -15,7 +15,7 @@ import Error from '../../Error'; import useFieldType from '../../useFieldType'; import Popup from '../../../elements/Popup'; import BlockSelector from './BlockSelector'; -import { blocks } from '../../../../../fields/validations'; +import { blocks as blocksValidator } from '../../../../../fields/validations'; import './index.scss'; @@ -71,7 +71,7 @@ const Blocks = (props) => { const { customComponentsPath } = useRenderedFields(); const { getDataByPath } = useForm(); - const addRow = (index, blockType) => { + const addRow = useCallback((index, blockType) => { const data = getDataByPath(path); dispatchRows({ @@ -79,9 +79,9 @@ const Blocks = (props) => { }); setValue(value + 1); - }; + }, [getDataByPath, path, setValue, value]); - const removeRow = (index) => { + const removeRow = useCallback((index) => { const data = getDataByPath(path); dispatchRows({ @@ -91,28 +91,28 @@ const Blocks = (props) => { }); setValue(value - 1); - }; + }, [getDataByPath, path, setValue, value]); - const moveRow = (moveFromIndex, moveToIndex) => { + const moveRow = useCallback((moveFromIndex, moveToIndex) => { const data = getDataByPath(path); dispatchRows({ type: 'MOVE', index: moveFromIndex, moveToIndex, data, }); - }; + }, [getDataByPath, path]); - const toggleCollapse = (index) => { + const toggleCollapse = useCallback((index) => { dispatchRows({ type: 'TOGGLE_COLLAPSE', index, rows, }); - }; + }, [rows]); - const onDragEnd = (result) => { + const onDragEnd = useCallback((result) => { if (!result.destination) return; const sourceIndex = result.source.index; const destinationIndex = result.destination.index; moveRow(sourceIndex, destinationIndex); - }; + }, [moveRow]); useEffect(() => { dispatchRows({ @@ -138,6 +138,87 @@ const Blocks = (props) => { } }, [value, setValue, disableFormData, dataToInitialize]); + return ( + + ); +}; + +Blocks.defaultProps = { + label: '', + defaultValue: [], + initialData: [], + singularLabel: 'Block', + validate: blocksValidator, + required: false, + maxRows: undefined, + minRows: undefined, + permissions: {}, +}; + +Blocks.propTypes = { + blocks: PropTypes.arrayOf( + PropTypes.shape({}), + ).isRequired, + defaultValue: PropTypes.arrayOf( + PropTypes.shape({}), + ), + initialData: PropTypes.arrayOf( + PropTypes.shape({}), + ), + label: PropTypes.string, + singularLabel: PropTypes.string, + name: PropTypes.string.isRequired, + path: PropTypes.string.isRequired, + fieldTypes: PropTypes.shape({}).isRequired, + validate: PropTypes.func, + required: PropTypes.bool, + maxRows: PropTypes.number, + minRows: PropTypes.number, + permissions: PropTypes.shape({ + fields: PropTypes.shape({}), + }), +}; + +const RenderBlock = React.memo((props) => { + const { + onDragEnd, + label, + showError, + errorMessage, + rows, + singularLabel, + addRow, + removeRow, + moveRow, + path, + customComponentsPath, + name, + fieldTypes, + permissions, + value, + toggleCollapse, + dataToInitialize, + blocks, + } = props; return ( @@ -235,45 +316,6 @@ const Blocks = (props) => {
    ); -}; - -Blocks.defaultProps = { - label: '', - defaultValue: [], - initialData: [], - singularLabel: 'Block', - validate: blocks, - required: false, - maxRows: undefined, - minRows: undefined, - permissions: {}, -}; - -Blocks.propTypes = { - blocks: PropTypes.arrayOf( - PropTypes.shape({}), - ).isRequired, - defaultValue: PropTypes.arrayOf( - PropTypes.shape({}), - ), - initialData: PropTypes.arrayOf( - PropTypes.shape({}), - ), - blocks: PropTypes.arrayOf( - PropTypes.shape({}), - ).isRequired, - label: PropTypes.string, - singularLabel: PropTypes.string, - name: PropTypes.string.isRequired, - path: PropTypes.string.isRequired, - fieldTypes: PropTypes.shape({}).isRequired, - validate: PropTypes.func, - required: PropTypes.bool, - maxRows: PropTypes.number, - minRows: PropTypes.number, - permissions: PropTypes.shape({ - fields: PropTypes.shape({}), - }), -}; +}); export default withCondition(Blocks); From d45ff316c0558d50e280c49a55847bdb76dbe316 Mon Sep 17 00:00:00 2001 From: James Date: Thu, 23 Jul 2020 19:16:01 -0400 Subject: [PATCH 118/125] implements intersection observer to mount fields over time --- .../components/forms/RenderFields/index.js | 135 ++++++++++-------- src/client/hooks/useIntersect.js | 29 ++++ 2 files changed, 106 insertions(+), 58 deletions(-) create mode 100644 src/client/hooks/useIntersect.js diff --git a/src/client/components/forms/RenderFields/index.js b/src/client/components/forms/RenderFields/index.js index 7f67c9156..f26135087 100644 --- a/src/client/components/forms/RenderFields/index.js +++ b/src/client/components/forms/RenderFields/index.js @@ -1,9 +1,14 @@ import React, { createContext, useEffect, useContext, useState } from 'react'; import PropTypes from 'prop-types'; import RenderCustomComponent from '../../utilities/RenderCustomComponent'; +import useIntersect from '../../../hooks/useIntersect'; import './index.scss'; +const intersectionObserverOptions = { + rootMargin: '1000px', +}; + const RenderedFieldContext = createContext({}); export const useRenderedFields = () => useContext(RenderedFieldContext); @@ -20,6 +25,10 @@ const RenderFields = (props) => { operation: operationFromProps, } = props; + const [hasIntersected, setHasIntersected] = useState(false); + const [intersectionRef, entry] = useIntersect(intersectionObserverOptions); + const isIntersecting = Boolean(entry?.isIntersecting); + const { customComponentsPath: customComponentsPathFromContext, operation: operationFromContext } = useRenderedFields(); const operation = operationFromProps || operationFromContext; @@ -37,75 +46,85 @@ const RenderFields = (props) => { }); }, [operation, customComponentsPath]); + useEffect(() => { + if (isIntersecting && !hasIntersected) { + setHasIntersected(true); + } + }, [isIntersecting, hasIntersected]); + if (fieldSchema) { return ( - - {fieldSchema.map((field, i) => { - if (!field?.hidden && field?.admin?.disabled !== true) { - if ((filter && typeof filter === 'function' && filter(field)) || !filter) { - const FieldComponent = field?.admin?.hidden ? fieldTypes.hidden : fieldTypes[field.type]; +
    + {hasIntersected && ( + + {fieldSchema.map((field, i) => { + if (!field?.hidden && field?.admin?.disabled !== true) { + if ((filter && typeof filter === 'function' && filter(field)) || !filter) { + const FieldComponent = field?.admin?.hidden ? fieldTypes.hidden : fieldTypes[field.type]; - let initialFieldData; - let fieldPermissions = permissions[field.name]; + let initialFieldData; + let fieldPermissions = permissions[field.name]; - if (!field.name) { - initialFieldData = initialData; - fieldPermissions = permissions; - } else if (initialData?.[field.name] !== undefined) { - initialFieldData = initialData[field.name]; - } + if (!field.name) { + initialFieldData = initialData; + fieldPermissions = permissions; + } else if (initialData?.[field.name] !== undefined) { + initialFieldData = initialData[field.name]; + } - let { admin: { readOnly } = {} } = field; + let { admin: { readOnly } = {} } = field; - if (readOnlyOverride) readOnly = true; + if (readOnlyOverride) readOnly = true; - if (permissions?.[field?.name]?.read?.permission !== false) { - if (permissions?.[field?.name]?.[operation]?.permission === false) { - readOnly = true; + if (permissions?.[field?.name]?.read?.permission !== false) { + if (permissions?.[field?.name]?.[operation]?.permission === false) { + readOnly = true; + } + + if (FieldComponent) { + return ( + + ); + } + + return ( +
    + No matched field found for + {' '} + " + {field.label} + " +
    + ); + } } - if (FieldComponent) { - return ( - - ); - } - - return ( -
    - No matched field found for - {' '} - " - {field.label} - " -
    - ); + return null; } - } - return null; - } - - return null; - })} -
    + return null; + })} + + )} +
    ); } diff --git a/src/client/hooks/useIntersect.js b/src/client/hooks/useIntersect.js new file mode 100644 index 000000000..b3ae43460 --- /dev/null +++ b/src/client/hooks/useIntersect.js @@ -0,0 +1,29 @@ +/* eslint-disable no-shadow */ +import { useEffect, useRef, useState } from 'react'; + +export default ({ root = null, rootMargin, threshold = 0 } = {}) => { + const [entry, updateEntry] = useState({}); + const [node, setNode] = useState(null); + + const observer = useRef( + new window.IntersectionObserver(([entry]) => updateEntry(entry), { + root, + rootMargin, + threshold, + }), + ); + + useEffect( + () => { + const { current: currentObserver } = observer; + currentObserver.disconnect(); + + if (node) currentObserver.observe(node); + + return () => currentObserver.disconnect(); + }, + [node], + ); + + return [setNode, entry]; +}; From 1be0890f0371608935acd0bd8a5f6d87b04cf795 Mon Sep 17 00:00:00 2001 From: James Date: Thu, 23 Jul 2020 19:16:16 -0400 Subject: [PATCH 119/125] 0.0.27 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 35e7f28e2..f447f8907 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@payloadcms/payload", - "version": "0.0.26", + "version": "0.0.27", "description": "CMS and Application Framework", "license": "ISC", "author": "Payload CMS LLC", From d38b8e746cc7ac261186147d91e295726162a1fc Mon Sep 17 00:00:00 2001 From: James Date: Thu, 23 Jul 2020 19:18:24 -0400 Subject: [PATCH 120/125] removes unused dependencies --- package.json | 3 --- 1 file changed, 3 deletions(-) diff --git a/package.json b/package.json index f447f8907..08fa48f4b 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,6 @@ "html-webpack-plugin": "^3.2.0", "http-status": "^1.4.2", "image-size": "^0.7.5", - "is-hotkey": "^0.1.6", "isomorphic-fetch": "^2.2.1", "jest": "^25.3.0", "jsonwebtoken": "^8.5.1", @@ -92,7 +91,6 @@ "react-document-meta": "^3.0.0-beta.2", "react-dom": "^16.13.1", "react-hook-form": "^5.7.2", - "react-redux": "^7.2.0", "react-router-dom": "^5.1.2", "react-router-navigation-prompt": "^1.8.11", "react-select": "^3.0.8", @@ -104,7 +102,6 @@ "slate-hyperscript": "^0.58.3", "slate-react": "^0.58.3", "style-loader": "^0.21.0", - "styled-components": "^5.1.1", "uglifyjs-webpack-plugin": "^2.2.0", "url-loader": "^1.0.1", "uuid": "^8.1.0", From deaf5cde5f11e10ec2623ab3103f8b3dedc75399 Mon Sep 17 00:00:00 2001 From: Elliot DeNolf Date: Thu, 23 Jul 2020 21:57:11 -0400 Subject: [PATCH 121/125] Initial logger implementation with pino --- demo/server.js | 12 ++++---- package.json | 3 ++ src/index.js | 2 ++ src/mongoose/connect.js | 5 ++-- src/utilities/logger.js | 10 +++++++ yarn.lock | 65 +++++++++++++++++++++++++++++++++++++---- 6 files changed, 85 insertions(+), 12 deletions(-) create mode 100644 src/utilities/logger.js diff --git a/demo/server.js b/demo/server.js index 0d79a33a6..1f69c31f5 100644 --- a/demo/server.js +++ b/demo/server.js @@ -2,6 +2,7 @@ const express = require('express'); const path = require('path'); const Payload = require('../src'); +const logger = require('../src/utilities/logger')('payload'); const expressApp = express(); @@ -15,7 +16,8 @@ const payload = new Payload({ mongoURL: 'mongodb://localhost/payload', express: expressApp, onInit: () => { - console.log('Payload is initialized'); + logger.info('Payload is initialized'); + // console.log('Payload is initialized'); }, }); @@ -37,13 +39,13 @@ exports.payload = payload; exports.start = (cb) => { const server = expressApp.listen(3000, async () => { - console.log(`listening on ${3000}...`); + logger.info(`listening on ${3000}...`); if (cb) cb(); const creds = await payload.getMockEmailCredentials(); - console.log(`Mock email account username: ${creds.user}`); - console.log(`Mock email account password: ${creds.pass}`); - console.log(`Log in to mock email provider at ${creds.web}`); + logger.info(`Mock email account username: ${creds.user}`); + logger.info(`Mock email account password: ${creds.pass}`); + logger.info(`Log in to mock email provider at ${creds.web}`); }); return server; diff --git a/package.json b/package.json index 458e2ffb3..740e90a27 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "express-fileupload": "^1.1.6", "express-graphql": "^0.9.0", "extract-text-webpack-plugin": "^4.0.0-beta.0", + "falsey": "^1.0.0", "file-loader": "^1.1.11", "flatley": "^5.2.0", "graphql": "^15.0.0", @@ -62,6 +63,7 @@ "jest": "^25.3.0", "jsonwebtoken": "^8.5.1", "method-override": "^3.0.0", + "micro-memoize": "^4.0.9", "minimist": "^1.2.0", "mkdirp": "^0.5.1", "mongodb-memory-server": "^6.5.2", @@ -79,6 +81,7 @@ "passport-jwt": "^4.0.0", "passport-local": "^1.0.0", "passport-local-mongoose": "^6.0.1", + "pino": "^6.4.1", "postcss-flexbugs-fixes": "^3.3.1", "postcss-loader": "^2.1.6", "postcss-preset-env": "6.0.6", diff --git a/src/index.js b/src/index.js index b1a3fc524..24f06a61d 100644 --- a/src/index.js +++ b/src/index.js @@ -3,6 +3,7 @@ require('isomorphic-fetch'); const express = require('express'); const graphQLPlayground = require('graphql-playground-middleware-express').default; +const logger = require('./utilities/logger')('payload'); const bindOperations = require('./init/bindOperations'); const bindRequestHandlers = require('./init/bindRequestHandlers'); const bindResolvers = require('./init/bindResolvers'); @@ -24,6 +25,7 @@ const performFieldOperations = require('./fields/performFieldOperations'); class Payload { constructor(options) { + logger.info('Starting Payload...'); const config = getConfig(options); this.config = sanitizeConfig(config); diff --git a/src/mongoose/connect.js b/src/mongoose/connect.js index a4647c8a2..322b8a63e 100644 --- a/src/mongoose/connect.js +++ b/src/mongoose/connect.js @@ -1,5 +1,6 @@ /* eslint-disable no-console */ const mongoose = require('mongoose'); +const logger = require('../utilities/logger')('payload'); const connectMongoose = async (url) => { let urlToConnect = url; @@ -19,9 +20,9 @@ const connectMongoose = async (url) => { useUnifiedTopology: true, useCreateIndex: true, }); - console.log(successfulConnectionMessage); + logger.info(successfulConnectionMessage); } catch (err) { - console.error('Error: cannot connect to MongoDB. Details: ', err); + logger.error('Error: cannot connect to MongoDB. Details: ', err); process.exit(1); } }; diff --git a/src/utilities/logger.js b/src/utilities/logger.js new file mode 100644 index 000000000..e01ec355e --- /dev/null +++ b/src/utilities/logger.js @@ -0,0 +1,10 @@ +const falsey = require('falsey'); +const pino = require('pino'); +const memoize = require('micro-memoize'); + +module.exports = memoize((name) => { + return pino({ + name, + enabled: falsey(process.env.DISABLE_LOGGING), + }); +}); diff --git a/yarn.lock b/yarn.lock index f0f336f6b..ffb7d2e44 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2081,6 +2081,11 @@ atob@^2.1.2: resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== +atomic-sleep@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/atomic-sleep/-/atomic-sleep-1.0.0.tgz#eb85b77a601fc932cfe432c5acd364a9e2c9075b" + integrity sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ== + autoprefixer@^9.1.5, autoprefixer@^9.7.4: version "9.8.4" resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.8.4.tgz#736f1012673a70fa3464671d78d41abd54512863" @@ -4582,6 +4587,11 @@ faker@^4.1.0: resolved "https://registry.yarnpkg.com/faker/-/faker-4.1.0.tgz#1e45bbbecc6774b3c195fad2835109c6d748cc3f" integrity sha1-HkW7vsxndLPBlfrSg1EJxtdIzD8= +"falsey@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/falsey/-/falsey-1.0.0.tgz#71bdd775c24edad9f2f5c015ce8be24400bb5d7d" + integrity sha512-zMDNZ/Ipd8MY0+346CPvhzP1AsiVyNfTOayJza4reAIWf72xbkuFUDcJNxSAsQE1b9Bu0wijKb8Ngnh/a7fI5w== + fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" @@ -4597,6 +4607,16 @@ fast-levenshtein@~2.0.6: resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= +fast-redact@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/fast-redact/-/fast-redact-2.0.0.tgz#17bb8f5e1f56ecf4a38c8455985e5eab4c478431" + integrity sha512-zxpkULI9W9MNTK2sJ3BpPQrTEXFNESd2X6O1tXMFpK/XM0G5c5Rll2EVYZH2TqI3xRGK/VaJ+eEOt7pnENJpeA== + +fast-safe-stringify@^2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz#124aa885899261f68aedb42a7c080de9da608743" + integrity sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA== + fast-shallow-equal@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fast-shallow-equal/-/fast-shallow-equal-1.0.0.tgz#d4dcaf6472440dcefa6f88b98e3251e27f25628b" @@ -4767,6 +4787,11 @@ flatley@^5.2.0: dependencies: is-buffer "^1.1.6" +flatstr@^1.0.12: + version "1.0.12" + resolved "https://registry.yarnpkg.com/flatstr/-/flatstr-1.0.12.tgz#c2ba6a08173edbb6c9640e3055b95e287ceb5931" + integrity sha512-4zPxDyhCyiN2wIAtSLI6gc82/EjqZc1onI4Mz/l0pWrAlsSfYH/2ZIcU+e3oA2wDwbzIWNKwa23F8rh6+DRWkw== + flatted@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.2.tgz#4575b21e2bcee7434aa9be662f4b7b5f9c2b5138" @@ -7032,6 +7057,11 @@ methods@~1.1.2: resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= +micro-memoize@^4.0.9: + version "4.0.9" + resolved "https://registry.yarnpkg.com/micro-memoize/-/micro-memoize-4.0.9.tgz#b44a38c9dffbee1cefc2fd139bc8947952268b62" + integrity sha512-Z2uZi/IUMGQDCXASdujXRqrXXEwSY0XffUrAOllhqzQI3wpUyZbiZTiE2JuYC0HSG2G7DbCS5jZmsEKEGZuemg== + micromatch@^3.1.10, micromatch@^3.1.4: version "3.1.10" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" @@ -7252,11 +7282,6 @@ mongodb@3.5.9, mongodb@^3.5.4: optionalDependencies: saslprep "^1.0.0" -mongoose-autopopulate@^0.11.0: - version "0.11.0" - resolved "https://registry.yarnpkg.com/mongoose-autopopulate/-/mongoose-autopopulate-0.11.0.tgz#9b5e9c9a36f63a39f8b205d12693e1d8a9260225" - integrity sha512-pbL6w8Yt3KF0yxHZqP498CHelygOSCvYo6SvO0Pz7UW3Mzu4/xIDdoazwnmodDeI81dlKNvpWTS8fVKSTM8sDg== - mongoose-hidden@^1.8.1: version "1.9.0" resolved "https://registry.yarnpkg.com/mongoose-hidden/-/mongoose-hidden-1.9.0.tgz#6cc7c0ce6e8134885f73ea7874061e88880f8d50" @@ -8190,6 +8215,23 @@ pinkie@^2.0.0: resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" integrity sha1-clVrgM+g1IqXToDnckjoDtT3+HA= +pino-std-serializers@^2.4.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/pino-std-serializers/-/pino-std-serializers-2.4.2.tgz#cb5e3e58c358b26f88969d7e619ae54bdfcc1ae1" + integrity sha512-WaL504dO8eGs+vrK+j4BuQQq6GLKeCCcHaMB2ItygzVURcL1CycwNEUHTD/lHFHs/NL5qAz2UKrjYWXKSf4aMQ== + +pino@^6.4.1: + version "6.4.1" + resolved "https://registry.yarnpkg.com/pino/-/pino-6.4.1.tgz#6173dcdc1e647ae17ddbc4a412d1f12d3d2ab9b5" + integrity sha512-1zDSQworQZw14tvqjuW5aj5GV5oUQpV5Bz5wnpVVltVPBzaOoV1Dv+oKn1xNCz2CCkOyZd+kkdlel9lCLBYl+Q== + dependencies: + fast-redact "^2.0.0" + fast-safe-stringify "^2.0.7" + flatstr "^1.0.12" + pino-std-serializers "^2.4.2" + quick-format-unescaped "^4.0.1" + sonic-boom "^1.0.0" + pirates@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.1.tgz#643a92caf894566f91b2b986d2c66950a8e2fb87" @@ -9060,6 +9102,11 @@ querystring@0.2.0, querystring@^0.2.0: resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA= +quick-format-unescaped@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/quick-format-unescaped/-/quick-format-unescaped-4.0.1.tgz#437a5ea1a0b61deb7605f8ab6a8fd3858dbeb701" + integrity sha512-RyYpQ6Q5/drsJyOhrWHYMWTedvjTIat+FTwv0K4yoUxzvekw2aRHMQJLlnvt8UantkZg2++bEzD9EdxXqkWf4A== + raf-schd@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-4.0.2.tgz#bd44c708188f2e84c810bf55fcea9231bcaed8a0" @@ -10230,6 +10277,14 @@ snapdragon@^0.8.1: source-map-resolve "^0.5.0" use "^3.1.0" +sonic-boom@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-1.0.2.tgz#8769646fac2ecb58bd0ed60562280aa4211598df" + integrity sha512-sRMmXu7uFDXoniGvtLHuQk5KWovLWoi6WKASn7rw0ro41mPf0fOolkGp4NE6680CbxvNh26zWNyFQYYWXe33EA== + dependencies: + atomic-sleep "^1.0.0" + flatstr "^1.0.12" + source-list-map@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34" From c9e37bcef5b1a7dc972f6da57d777d3fe89c8c35 Mon Sep 17 00:00:00 2001 From: Elliot DeNolf Date: Fri, 24 Jul 2020 09:36:22 -0400 Subject: [PATCH 122/125] Better formatting for logger output --- demo/server.js | 2 +- package.json | 1 + src/index.js | 2 +- src/mongoose/connect.js | 2 +- src/utilities/logger.js | 7 ++- yarn.lock | 96 ++++++++++++++++++++++++++++++++++++----- 6 files changed, 95 insertions(+), 15 deletions(-) diff --git a/demo/server.js b/demo/server.js index 1f69c31f5..240f2f367 100644 --- a/demo/server.js +++ b/demo/server.js @@ -2,7 +2,7 @@ const express = require('express'); const path = require('path'); const Payload = require('../src'); -const logger = require('../src/utilities/logger')('payload'); +const logger = require('../src/utilities/logger')(); const expressApp = express(); diff --git a/package.json b/package.json index 740e90a27..40984a22a 100644 --- a/package.json +++ b/package.json @@ -82,6 +82,7 @@ "passport-local": "^1.0.0", "passport-local-mongoose": "^6.0.1", "pino": "^6.4.1", + "pino-pretty": "^4.1.0", "postcss-flexbugs-fixes": "^3.3.1", "postcss-loader": "^2.1.6", "postcss-preset-env": "6.0.6", diff --git a/src/index.js b/src/index.js index 24f06a61d..86e753d44 100644 --- a/src/index.js +++ b/src/index.js @@ -3,7 +3,7 @@ require('isomorphic-fetch'); const express = require('express'); const graphQLPlayground = require('graphql-playground-middleware-express').default; -const logger = require('./utilities/logger')('payload'); +const logger = require('./utilities/logger')(); const bindOperations = require('./init/bindOperations'); const bindRequestHandlers = require('./init/bindRequestHandlers'); const bindResolvers = require('./init/bindResolvers'); diff --git a/src/mongoose/connect.js b/src/mongoose/connect.js index 322b8a63e..33fc7b65b 100644 --- a/src/mongoose/connect.js +++ b/src/mongoose/connect.js @@ -1,6 +1,6 @@ /* eslint-disable no-console */ const mongoose = require('mongoose'); -const logger = require('../utilities/logger')('payload'); +const logger = require('../utilities/logger')(); const connectMongoose = async (url) => { let urlToConnect = url; diff --git a/src/utilities/logger.js b/src/utilities/logger.js index e01ec355e..1c8586757 100644 --- a/src/utilities/logger.js +++ b/src/utilities/logger.js @@ -2,9 +2,14 @@ const falsey = require('falsey'); const pino = require('pino'); const memoize = require('micro-memoize'); -module.exports = memoize((name) => { +// eslint-disable-next-line arrow-body-style +module.exports = memoize((name = 'payload') => { return pino({ name, enabled: falsey(process.env.DISABLE_LOGGING), + prettyPrint: { + ignore: 'pid,hostname', + translateTime: 'HH:MM:ss', + }, }); }); diff --git a/yarn.lock b/yarn.lock index ffb7d2e44..bfcb75b5d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1071,6 +1071,11 @@ dependencies: prop-types "^15.7.2" +"@hapi/bourne@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@hapi/bourne/-/bourne-2.0.0.tgz#5bb2193eb685c0007540ca61d166d4e1edaf918d" + integrity sha512-WEezM1FWztfbzqIUbsDzFRVMxSoLy3HugVcux6KDDtTqzPsLE8NDRHfXvev66aH1i2oOKKar3/XDjbvh/OUBdg== + "@istanbuljs/load-nyc-config@^1.0.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced" @@ -1916,6 +1921,16 @@ argparse@^1.0.7: dependencies: sprintf-js "~1.0.2" +args@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/args/-/args-5.0.1.tgz#4bf298df90a4799a09521362c579278cc2fdd761" + integrity sha512-1kqmFCFsPffavQFGt8OxJdIcETti99kySRUPMpOhaGjL6mRJn8HFU1OxKY5bMqfZKUwTQc1mZkAjmGYaVOHFtQ== + dependencies: + camelcase "5.0.0" + chalk "2.4.2" + leven "2.1.0" + mri "1.1.4" + aria-query@^4.2.2: version "4.2.2" resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-4.2.2.tgz#0d2ca6c9aceb56b8977e9fed6aed7e15bbd2f83b" @@ -2683,6 +2698,11 @@ camelcase-keys@^2.0.0: camelcase "^2.0.0" map-obj "^1.0.0" +camelcase@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.0.0.tgz#03295527d58bd3cd4aa75363f35b2e8d97be2f42" + integrity sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA== + camelcase@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f" @@ -2740,6 +2760,15 @@ caseless@~0.12.0: resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= +chalk@2.4.2, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.4.1, chalk@^2.4.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + chalk@^1.1.1, chalk@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" @@ -2751,15 +2780,6 @@ chalk@^1.1.1, chalk@^1.1.3: strip-ansi "^3.0.0" supports-color "^2.0.0" -chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.4.1, chalk@^2.4.2: - version "2.4.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" - integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== - dependencies: - ansi-styles "^3.2.1" - escape-string-regexp "^1.0.5" - supports-color "^5.3.0" - chalk@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4" @@ -2768,7 +2788,7 @@ chalk@^3.0.0: ansi-styles "^4.1.0" supports-color "^7.1.0" -chalk@^4.1.0: +chalk@^4.0.0, chalk@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.0.tgz#4e14870a618d9e2edd97dd8345fd9d9dc315646a" integrity sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A== @@ -3610,6 +3630,11 @@ date-fns@^2.0.1, date-fns@^2.14.0: resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.14.0.tgz#359a87a265bb34ef2e38f93ecf63ac453f9bc7ba" integrity sha512-1zD+68jhFgDIM0rF05rcwYO8cExdNqxjq4xP1QKM60Q45mnO6zaMWB4tOzrIr4M4GSLntsKeE4c9Bdl2jhL/yw== +dateformat@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae" + integrity sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q== + debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" @@ -6429,6 +6454,16 @@ jest@^25.3.0: import-local "^3.0.2" jest-cli "^25.5.4" +jmespath@^0.15.0: + version "0.15.0" + resolved "https://registry.yarnpkg.com/jmespath/-/jmespath-0.15.0.tgz#a3f222a9aae9f966f5d27c796510e28091764217" + integrity sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc= + +joycon@^2.2.5: + version "2.2.5" + resolved "https://registry.yarnpkg.com/joycon/-/joycon-2.2.5.tgz#8d4cf4cbb2544d7b7583c216fcdfec19f6be1615" + integrity sha512-YqvUxoOcVPnCp0VU1/56f+iKSdvIRJYPznH22BdXV3xMk75SFXhWeJkZ8C9XxUWt1b5x2X1SxuFygW1U0FmkEQ== + js-base64@^2.1.8: version "2.6.2" resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.6.2.tgz#cf9301bc5cc756892a9a6c8d7138322e5944fb0d" @@ -6674,6 +6709,11 @@ lcid@^1.0.0: dependencies: invert-kv "^1.0.0" +leven@2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/leven/-/leven-2.1.0.tgz#c2e7a9f772094dee9d34202ae8acce4687875580" + integrity sha1-wuep93IJTe6dNCAq6KzORoeHVYA= + leven@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" @@ -7344,6 +7384,11 @@ mquery@3.2.2: safe-buffer "5.1.2" sliced "1.0.1" +mri@1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/mri/-/mri-1.1.4.tgz#7cb1dd1b9b40905f1fac053abe25b6720f44744a" + integrity sha512-6y7IjGPm8AzlvoUrwAaw1tLnUBudaS3752vcd8JtrpGGQn+rXIe63LFVHm/YMwtqAuh+LJPCFdlLYPWM1nYn6w== + ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" @@ -8215,6 +8260,23 @@ pinkie@^2.0.0: resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" integrity sha1-clVrgM+g1IqXToDnckjoDtT3+HA= +pino-pretty@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/pino-pretty/-/pino-pretty-4.1.0.tgz#ad47ae00d4f3959cfadcdd691b1ebdcbdd61e42e" + integrity sha512-D4rORz/7SCZ0AGeWw9QFXsJ+ky3jDOqi/ABFzxxCk5BtEelEgJ6AABFew7TosHGw7I1t8VuMaGdTVNoYMZc+jg== + dependencies: + "@hapi/bourne" "^2.0.0" + args "^5.0.1" + chalk "^4.0.0" + dateformat "^3.0.3" + fast-safe-stringify "^2.0.7" + jmespath "^0.15.0" + joycon "^2.2.5" + pump "^3.0.0" + readable-stream "^3.6.0" + split2 "^3.1.1" + strip-json-comments "^3.1.1" + pino-std-serializers@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/pino-std-serializers/-/pino-std-serializers-2.4.2.tgz#cb5e3e58c358b26f88969d7e619ae54bdfcc1ae1" @@ -9423,7 +9485,7 @@ read-pkg@^5.2.0: string_decoder "~1.1.1" util-deprecate "~1.0.1" -readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0: +readable-stream@^3.0.0, readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== @@ -10386,6 +10448,13 @@ split-string@^3.0.1, split-string@^3.0.2: dependencies: extend-shallow "^3.0.0" +split2@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/split2/-/split2-3.1.1.tgz#c51f18f3e06a8c4469aaab487687d8d956160bb6" + integrity sha512-emNzr1s7ruq4N+1993yht631/JH+jaj0NYBosuKmLcq+JkGQ9MmTw1RB1fGaTCzUuseRIClrlSLHRNYGwWQ58Q== + dependencies: + readable-stream "^3.0.0" + sprintf-js@~1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" @@ -10666,6 +10735,11 @@ strip-json-comments@^3.0.1: resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.0.tgz#7638d31422129ecf4457440009fba03f9f9ac180" integrity sha512-e6/d0eBu7gHtdCqFt0xJr642LdToM5/cN4Qb9DbHjVx1CP5RyeM+zH7pbecEmDv/lBqb0QH+6Uqq75rxFPkM0w== +strip-json-comments@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== + strip-json-comments@~2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" From cc4794e5ac9b191782bd2d0d7e6fe9c45263f3ab Mon Sep 17 00:00:00 2001 From: James Date: Mon, 27 Jul 2020 11:53:00 -0400 Subject: [PATCH 123/125] implements initial form state --- demo/collections/Localized.js | 2 +- .../components/elements/Paginator/index.scss | 2 +- .../SectionTitle/EditableBlockTitle/index.js | 12 +- .../forms/DraggableSection/index.js | 9 +- .../forms/Form/buildStateFromSchema.js | 101 +++++++ .../components/forms/Form/fieldReducer.js | 143 +++++++++- .../components/forms/Form/flattenFilters.js | 10 + src/client/components/forms/Form/index.js | 248 ++++++++++-------- .../components/forms/Form/initContextState.js | 2 + .../components/forms/RenderFields/index.js | 25 +- .../components/forms/RenderFields/index.scss | 3 - .../forms/field-types/Array/index.js | 111 ++++---- .../BlockSelector/BlockSelection/index.js | 9 +- .../BlockSelector/BlockSelection/index.scss | 4 + .../forms/field-types/Blocks/index.js | 142 +++++----- .../forms/field-types/Checkbox/index.js | 8 - .../forms/field-types/DateTime/index.js | 8 - .../forms/field-types/Email/index.js | 5 - .../forms/field-types/Group/index.js | 5 +- .../forms/field-types/HiddenInput/index.js | 8 - .../forms/field-types/Number/index.js | 9 - .../forms/field-types/Password/index.js | 8 - .../forms/field-types/RadioGroup/index.js | 10 +- .../forms/field-types/Relationship/index.js | 44 +--- .../forms/field-types/RichText/index.js | 8 - .../components/forms/field-types/Row/index.js | 26 +- .../forms/field-types/Select/index.js | 14 - .../forms/field-types/Text/index.js | 9 - .../forms/field-types/Textarea/index.js | 17 +- .../forms/field-types/Upload/index.js | 25 +- .../components/forms/field-types/index.js | 3 - .../forms/field-types/rowReducer.js | 62 +++-- .../components/forms/useFieldType/index.js | 54 ++-- src/client/components/views/Global/Default.js | 13 +- src/client/components/views/Global/index.js | 4 + .../collections/Edit}/Auth/APIKey.js | 34 +-- .../collections/Edit}/Auth/index.js | 34 ++- .../collections/Edit}/Auth/index.scss | 4 +- .../views/collections/Edit/Default.js | 51 +++- .../collections/Edit/Upload}/index.js | 24 +- .../collections/Edit/Upload}/index.scss | 2 +- .../views/collections/Edit/index.js | 42 ++- .../views/collections/List/Default.js | 4 +- 43 files changed, 733 insertions(+), 625 deletions(-) create mode 100644 src/client/components/forms/Form/buildStateFromSchema.js create mode 100644 src/client/components/forms/Form/flattenFilters.js delete mode 100644 src/client/components/forms/RenderFields/index.scss rename src/client/components/{forms/field-types => views/collections/Edit}/Auth/APIKey.js (71%) rename src/client/components/{forms/field-types => views/collections/Edit}/Auth/index.js (70%) rename src/client/components/{forms/field-types => views/collections/Edit}/Auth/index.scss (73%) rename src/client/components/{forms/field-types/File => views/collections/Edit/Upload}/index.js (92%) rename src/client/components/{forms/field-types/File => views/collections/Edit/Upload}/index.scss (96%) diff --git a/demo/collections/Localized.js b/demo/collections/Localized.js index 2e254578c..7ba13b601 100644 --- a/demo/collections/Localized.js +++ b/demo/collections/Localized.js @@ -16,7 +16,7 @@ module.exports = { read: () => true, }, preview: (doc, token) => { - if (doc.title) { + if (doc && doc.title) { return `http://localhost:3000/posts/${doc.title.value}?preview=true&token=${token}`; } diff --git a/src/client/components/elements/Paginator/index.scss b/src/client/components/elements/Paginator/index.scss index c31fda577..4d927de37 100644 --- a/src/client/components/elements/Paginator/index.scss +++ b/src/client/components/elements/Paginator/index.scss @@ -29,7 +29,7 @@ outline: 0; padding: base(.5); color: $color-dark-gray; - line-height: 1; + line-height: base(1); &:hover:not(.clickable-arrow--is-disabled) { background: $color-background-gray; diff --git a/src/client/components/forms/DraggableSection/SectionTitle/EditableBlockTitle/index.js b/src/client/components/forms/DraggableSection/SectionTitle/EditableBlockTitle/index.js index c4f8247b2..fb588e09c 100644 --- a/src/client/components/forms/DraggableSection/SectionTitle/EditableBlockTitle/index.js +++ b/src/client/components/forms/DraggableSection/SectionTitle/EditableBlockTitle/index.js @@ -8,7 +8,7 @@ import './index.scss'; const baseClass = 'editable-block-title'; const EditableBlockTitle = (props) => { - const { path, initialData } = props; + const { path } = props; const inputRef = useRef(null); const inputCloneRef = useRef(null); const [inputWidth, setInputWidth] = useState(0); @@ -18,7 +18,6 @@ const EditableBlockTitle = (props) => { setValue, } = useFieldType({ path, - initialData, }); useEffect(() => { @@ -31,7 +30,7 @@ const EditableBlockTitle = (props) => { }; return ( - <> +
    { > {value || 'Untitled'} - + ); }; -EditableBlockTitle.defaultProps = { - initialData: undefined, -}; - EditableBlockTitle.propTypes = { path: PropTypes.string.isRequired, - initialData: PropTypes.string, }; export default EditableBlockTitle; diff --git a/src/client/components/forms/DraggableSection/index.js b/src/client/components/forms/DraggableSection/index.js index f71ce1ea2..d702c3a1f 100644 --- a/src/client/components/forms/DraggableSection/index.js +++ b/src/client/components/forms/DraggableSection/index.js @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import PropTypes from 'prop-types'; import AnimateHeight from 'react-animate-height'; import { Draggable } from 'react-beautiful-dnd'; @@ -22,17 +22,16 @@ const DraggableSection = (props) => { rowCount, parentPath, fieldSchema, - initialData, singularLabel, blockType, fieldTypes, customComponentsPath, - isOpen, + toggleRowCollapse, id, positionPanelVerticalAlignment, actionPanelVerticalAlignment, - toggleRowCollapse, permissions, + isOpen, } = props; const [isHovered, setIsHovered] = useState(false); @@ -73,7 +72,6 @@ const DraggableSection = (props) => {
    @@ -92,7 +90,6 @@ const DraggableSection = (props) => { duration={0} > { + const validatedFieldState = fieldState; + + validatedFieldState.valid = typeof field.validate === 'function' ? await field.validate(fieldState.value, field) : true; + + if (typeof validatedFieldState.valid === 'string') { + validatedFieldState.errorMessage = validatedFieldState.valid; + validatedFieldState.valid = false; + } +}; + +const buildStateFromSchema = async (fieldSchema, fullData) => { + if (fieldSchema && fullData) { + const validationPromises = []; + + const structureFieldState = (field, data = {}) => { + const value = data[field.name] || field.defaultValue; + + const fieldState = { + value, + initialValue: value, + }; + + validationPromises.push(buildValidationPromise(fieldState, field)); + + return fieldState; + }; + + const iterateFields = (fields, data, path = '') => fields.reduce((state, field) => { + if (field.name && data[field.name]) { + if (Array.isArray(data[field.name])) { + if (field.type === 'array') { + return { + ...state, + ...data[field.name].reduce((rowState, row, i) => ({ + ...rowState, + ...iterateFields(field.fields, row, `${path}${field.name}.${i}.`), + }), {}), + }; + } + + if (field.type === 'blocks') { + return { + ...state, + ...data[field.name].reduce((rowState, row, i) => { + const block = field.blocks.find((blockType) => blockType.slug === row.blockType); + const rowPath = `${path}${field.name}.${i}.`; + + return { + ...rowState, + [`${rowPath}blockType`]: { + value: row.blockType, + initialValue: row.blockType, + valid: true, + }, + [`${rowPath}blockName`]: { + value: row.blockName, + initialValue: row.blockName, + valid: true, + }, + ...iterateFields(block.fields, row, rowPath), + }; + }, {}), + }; + } + } + + if (field.fields) { + return { + ...state, + ...iterateFields(field.fields, data[field.name], `${path}${field.name}.`), + }; + } + + + return { + ...state, + [`${path}${field.name}`]: structureFieldState(field, data), + }; + } + + if (field.fields) { + return { + ...state, + ...iterateFields(field.fields, data, path), + }; + } + + return state; + }, {}); + + const resultingState = iterateFields(fieldSchema, fullData); + await Promise.all(validationPromises); + return resultingState; + } + + return {}; +}; + + +module.exports = buildStateFromSchema; diff --git a/src/client/components/forms/Form/fieldReducer.js b/src/client/components/forms/Form/fieldReducer.js index a32d0d61d..dc8aeb704 100644 --- a/src/client/components/forms/Form/fieldReducer.js +++ b/src/client/components/forms/Form/fieldReducer.js @@ -1,9 +1,43 @@ +import { unflatten, flatten } from 'flatley'; +import flattenFilters from './flattenFilters'; + +// +const unflattenRowsFromState = (state, path) => { + // Take a copy of state + const remainingFlattenedState = { ...state }; + + const rowsFromStateObject = {}; + + const pathPrefixToRemove = path.substring(0, path.lastIndexOf('.') + 1); + + // Loop over all keys from state + // If the key begins with the name of the parent field, + // Add value to rowsFromStateObject and delete it from remaining state + Object.keys(state).forEach((key) => { + if (key.indexOf(`${path}.`) === 0) { + if (!state[key].ignoreWhileFlattening) { + const name = key.replace(pathPrefixToRemove, ''); + rowsFromStateObject[name] = state[key]; + rowsFromStateObject[name].initialValue = rowsFromStateObject[name].value; + } + + delete remainingFlattenedState[key]; + } + }); + + const unflattenedRows = unflatten(rowsFromStateObject); + + return { + unflattenedRows: unflattenedRows[path.replace(pathPrefixToRemove, '')] || [], + remainingFlattenedState, + }; +}; + function fieldReducer(state, action) { switch (action.type) { - case 'REPLACE_ALL': - return { - ...action.value, - }; + case 'REPLACE_STATE': { + return action.state; + } case 'REMOVE': { const newState = { ...state }; @@ -11,15 +45,112 @@ function fieldReducer(state, action) { return newState; } + case 'REMOVE_ROW': { + const { rowIndex, path } = action; + const { unflattenedRows, remainingFlattenedState } = unflattenRowsFromState(state, path); + + unflattenedRows.splice(rowIndex, 1); + + const flattenedRowState = unflattenedRows.length > 0 ? flatten({ [path]: unflattenedRows }, { filters: flattenFilters }) : {}; + + return { + ...remainingFlattenedState, + ...flattenedRowState, + }; + } + + case 'ADD_ROW': { + const { + rowIndex, path, fieldSchema, blockType, + } = action; + const { unflattenedRows, remainingFlattenedState } = unflattenRowsFromState(state, path); + + // Get paths of sub fields + const subFields = fieldSchema.reduce((acc, field) => { + if (field.type === 'flexible' || field.type === 'repeater') { + return acc; + } + + if (field.name) { + return { + ...acc, + [field.name]: { + value: null, + valid: !field.required, + }, + }; + } + + if (field.fields) { + return { + ...acc, + ...(field.fields.reduce((fields, subField) => ({ + ...fields, + [subField.name]: { + value: null, + valid: !field.required, + }, + }), {})), + }; + } + + return acc; + }, {}); + + if (blockType) { + subFields.blockType = { + value: blockType, + initialValue: blockType, + valid: true, + }; + + subFields.blockName = { + value: null, + initialValue: null, + valid: true, + }; + } + + // Add new object containing subfield names to unflattenedRows array + unflattenedRows.splice(rowIndex + 1, 0, subFields); + + const newState = { + ...remainingFlattenedState, + ...(flatten({ [path]: unflattenedRows }, { filters: flattenFilters })), + }; + + return newState; + } + + case 'MOVE_ROW': { + const { moveFromIndex, moveToIndex, path } = action; + const { unflattenedRows, remainingFlattenedState } = unflattenRowsFromState(state, path); + + // copy the row to move + const copyOfMovingRow = unflattenedRows[moveFromIndex]; + // delete the row by index + unflattenedRows.splice(moveFromIndex, 1); + // insert row copyOfMovingRow back in + unflattenedRows.splice(moveToIndex, 0, copyOfMovingRow); + + const newState = { + ...remainingFlattenedState, + ...(flatten({ [path]: unflattenedRows }, { filters: flattenFilters })), + }; + + return newState; + } + default: { const newField = { value: action.value, valid: action.valid, errorMessage: action.errorMessage, + disableFormData: action.disableFormData, + ignoreWhileFlattening: action.ignoreWhileFlattening, + initialValue: action.initialValue, }; - if (action.disableFormData) newField.disableFormData = action.disableFormData; - return { ...state, [action.path]: newField, diff --git a/src/client/components/forms/Form/flattenFilters.js b/src/client/components/forms/Form/flattenFilters.js new file mode 100644 index 000000000..bfd3972c5 --- /dev/null +++ b/src/client/components/forms/Form/flattenFilters.js @@ -0,0 +1,10 @@ +const flattenFilters = [{ + test: (_, value) => { + const hasValidProperty = Object.prototype.hasOwnProperty.call(value, 'valid'); + const hasValueProperty = Object.prototype.hasOwnProperty.call(value, 'value'); + + return (hasValidProperty && hasValueProperty); + }, +}]; + +export default flattenFilters; diff --git a/src/client/components/forms/Form/index.js b/src/client/components/forms/Form/index.js index 40aac23fb..8403f1811 100644 --- a/src/client/components/forms/Form/index.js +++ b/src/client/components/forms/Form/index.js @@ -1,11 +1,10 @@ import React, { - useReducer, useEffect, useRef, useState, + useReducer, useEffect, useRef, useState, useCallback, } from 'react'; import { objectToFormData } from 'object-to-formdata'; import { useHistory } from 'react-router-dom'; import PropTypes from 'prop-types'; import { unflatten } from 'flatley'; -import HiddenInput from '../field-types/HiddenInput'; import { useLocale } from '../../utilities/Locale'; import { useStatusList } from '../../elements/Status'; import { requests } from '../../../api'; @@ -40,7 +39,6 @@ const Form = (props) => { const { disabled, onSubmit, - ajax, method, action, handleResponse, @@ -49,6 +47,7 @@ const Form = (props) => { className, redirect, disableSuccessStatus, + initialState, } = props; const history = useHistory(); @@ -62,11 +61,12 @@ const Form = (props) => { const contextRef = useRef({ ...initContextState }); + contextRef.current.initialState = initialState; + const [fields, dispatchFields] = useReducer(fieldReducer, {}); contextRef.current.fields = fields; - contextRef.current.dispatchFields = dispatchFields; - contextRef.current.submit = (e) => { + const submit = useCallback((e) => { if (disabled) { e.preventDefault(); return false; @@ -100,121 +100,129 @@ const Form = (props) => { return onSubmit(fields); } - // If form is AJAX, fetch data - if (ajax !== false) { - e.preventDefault(); + e.preventDefault(); - window.scrollTo({ - top: 0, - behavior: 'smooth', - }); + window.scrollTo({ + top: 0, + behavior: 'smooth', + }); - const formData = contextRef.current.createFormData(); - setProcessing(true); + const formData = contextRef.current.createFormData(); + setProcessing(true); - // Make the API call from the action - return requests[method.toLowerCase()](action, { - body: formData, - }).then((res) => { - setModified(false); - if (typeof handleResponse === 'function') return handleResponse(res); + // Make the API call from the action + return requests[method.toLowerCase()](action, { + body: formData, + }).then((res) => { + setModified(false); + if (typeof handleResponse === 'function') return handleResponse(res); - return res.json().then((json) => { - setProcessing(false); - clearStatus(); + return res.json().then((json) => { + setProcessing(false); + clearStatus(); - if (res.status < 400) { - if (typeof onSuccess === 'function') onSuccess(json); + if (res.status < 400) { + if (typeof onSuccess === 'function') onSuccess(json); - if (redirect) { - const destination = { - pathname: redirect, + if (redirect) { + const destination = { + pathname: redirect, + }; + + if (json.message && !disableSuccessStatus) { + destination.state = { + status: [ + { + message: json.message, + type: 'success', + }, + ], }; - - if (json.message && !disableSuccessStatus) { - destination.state = { - status: [ - { - message: json.message, - type: 'success', - }, - ], - }; - } - - history.push(destination); - } else if (!disableSuccessStatus) { - replaceStatus([{ - message: json.message, - type: 'success', - disappear: 3000, - }]); - } - } else { - if (json.message) { - addStatus({ - message: json.message, - type: 'error', - }); - - return json; - } - - if (Array.isArray(json.errors)) { - const [fieldErrors, nonFieldErrors] = json.errors.reduce(([fieldErrs, nonFieldErrs], err) => (err.field && err.message ? [[...fieldErrs, err], nonFieldErrs] : [fieldErrs, [...nonFieldErrs, err]]), [[], []]); - - fieldErrors.forEach((err) => { - dispatchFields({ - valid: false, - errorMessage: err.message, - path: err.field, - value: contextRef.current.fields?.[err.field]?.value, - }); - }); - - nonFieldErrors.forEach((err) => { - addStatus({ - message: err.message || 'An unknown error occurred.', - type: 'error', - }); - }); - - if (fieldErrors.length > 0 && nonFieldErrors.length === 0) { - addStatus({ - message: 'Please correct the fields below.', - type: 'error', - }); - } - - return json; } + history.push(destination); + } else if (!disableSuccessStatus) { + replaceStatus([{ + message: json.message, + type: 'success', + disappear: 3000, + }]); + } + } else { + if (json.message) { addStatus({ - message: 'An unknown error occurred.', + message: json.message, type: 'error', }); + + return json; } - return json; - }); - }).catch((err) => { - addStatus({ - message: err, - type: 'error', - }); + if (Array.isArray(json.errors)) { + const [fieldErrors, nonFieldErrors] = json.errors.reduce(([fieldErrs, nonFieldErrs], err) => (err.field && err.message ? [[...fieldErrs, err], nonFieldErrs] : [fieldErrs, [...nonFieldErrs, err]]), [[], []]); + + fieldErrors.forEach((err) => { + dispatchFields({ + ...(contextRef.current?.fields?.[err.field] || {}), + valid: false, + errorMessage: err.message, + path: err.field, + }); + }); + + nonFieldErrors.forEach((err) => { + addStatus({ + message: err.message || 'An unknown error occurred.', + type: 'error', + }); + }); + + if (fieldErrors.length > 0 && nonFieldErrors.length === 0) { + addStatus({ + message: 'Please correct the fields below.', + type: 'error', + }); + } + + return json; + } + + addStatus({ + message: 'An unknown error occurred.', + type: 'error', + }); + } + + return json; }); - } + }).catch((err) => { + addStatus({ + message: err, + type: 'error', + }); + }); + }, [ + action, + addStatus, + clearStatus, + disableSuccessStatus, + disabled, + fields, + handleResponse, + history, + method, + onSubmit, + onSuccess, + redirect, + replaceStatus, + ]); - return true; - }; - contextRef.current.getFields = () => contextRef.current.fields; + const getFields = useCallback(() => contextRef.current.fields, [contextRef]); + const getField = useCallback((path) => contextRef.current.fields[path], [contextRef]); + const getData = useCallback(() => reduceFieldsToValues(contextRef.current.fields, true), [contextRef]); - contextRef.current.getField = (path) => contextRef.current.fields[path]; - - contextRef.current.getData = () => reduceFieldsToValues(contextRef.current.fields, true); - - contextRef.current.getSiblingData = (path) => { + const getSiblingData = useCallback((path) => { let siblingFields = contextRef.current.fields; // If this field is nested @@ -234,9 +242,9 @@ const Form = (props) => { } return reduceFieldsToValues(siblingFields, true); - }; + }, [contextRef]); - contextRef.current.getDataByPath = (path) => { + const getDataByPath = useCallback((path) => { const pathPrefixToRemove = path.substring(0, path.lastIndexOf('.') + 1); const name = path.split('.').pop(); @@ -254,21 +262,35 @@ const Form = (props) => { const values = reduceFieldsToValues(data, true); const unflattenedData = unflatten(values); return unflattenedData?.[name]; - }; + }, [contextRef]); - contextRef.current.getUnflattenedValues = () => reduceFieldsToValues(contextRef.current.fields); + const getUnflattenedValues = useCallback(() => reduceFieldsToValues(contextRef.current.fields), [contextRef]); - contextRef.current.validateForm = () => !Object.values(contextRef.current.fields).some((field) => field.valid === false); + const validateForm = useCallback(() => !Object.values(contextRef.current.fields).some((field) => field.valid === false), [contextRef]); - contextRef.current.createFormData = () => { + const createFormData = useCallback(() => { const data = reduceFieldsToValues(contextRef.current.fields); return objectToFormData(data, { indices: true }); - }; + }, [contextRef]); + contextRef.current.dispatchFields = dispatchFields; + contextRef.current.submit = submit; + contextRef.current.getFields = getFields; + contextRef.current.getField = getField; + contextRef.current.getData = getData; + contextRef.current.getSiblingData = getSiblingData; + contextRef.current.getDataByPath = getDataByPath; + contextRef.current.getUnflattenedValues = getUnflattenedValues; + contextRef.current.validateForm = validateForm; + contextRef.current.createFormData = createFormData; contextRef.current.setModified = setModified; contextRef.current.setProcessing = setProcessing; contextRef.current.setSubmitted = setSubmitted; + useEffect(() => { + dispatchFields({ type: 'REPLACE_STATE', state: initialState }); + }, [initialState]); + useThrottledEffect(() => { refreshCookie(); }, 15000, [fields]); @@ -299,10 +321,6 @@ const Form = (props) => { - {children} @@ -317,7 +335,6 @@ const Form = (props) => { Form.defaultProps = { redirect: '', onSubmit: null, - ajax: true, method: 'POST', action: '', handleResponse: null, @@ -325,12 +342,12 @@ Form.defaultProps = { className: '', disableSuccessStatus: false, disabled: false, + initialState: {}, }; Form.propTypes = { disableSuccessStatus: PropTypes.bool, onSubmit: PropTypes.func, - ajax: PropTypes.bool, method: PropTypes.oneOf(['post', 'POST', 'get', 'GET', 'put', 'PUT', 'delete', 'DELETE']), action: PropTypes.string, handleResponse: PropTypes.func, @@ -342,6 +359,7 @@ Form.propTypes = { className: PropTypes.string, redirect: PropTypes.string, disabled: PropTypes.bool, + initialState: PropTypes.shape({}), }; export default Form; diff --git a/src/client/components/forms/Form/initContextState.js b/src/client/components/forms/Form/initContextState.js index ff449921b..d72307cc9 100644 --- a/src/client/components/forms/Form/initContextState.js +++ b/src/client/components/forms/Form/initContextState.js @@ -10,4 +10,6 @@ export default { submit: () => { }, dispatchFields: () => { }, setModified: () => { }, + initialState: {}, + reset: 0, }; diff --git a/src/client/components/forms/RenderFields/index.js b/src/client/components/forms/RenderFields/index.js index f26135087..54e708031 100644 --- a/src/client/components/forms/RenderFields/index.js +++ b/src/client/components/forms/RenderFields/index.js @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import RenderCustomComponent from '../../utilities/RenderCustomComponent'; import useIntersect from '../../../hooks/useIntersect'; -import './index.scss'; +const baseClass = 'render-fields'; const intersectionObserverOptions = { rootMargin: '1000px', @@ -16,13 +16,13 @@ export const useRenderedFields = () => useContext(RenderedFieldContext); const RenderFields = (props) => { const { fieldSchema, - initialData, customComponentsPath: customComponentsPathFromProps, fieldTypes, filter, permissions, readOnly: readOnlyOverride, operation: operationFromProps, + className, } = props; const [hasIntersected, setHasIntersected] = useState(false); @@ -52,9 +52,17 @@ const RenderFields = (props) => { } }, [isIntersecting, hasIntersected]); + const classes = [ + baseClass, + className, + ].filter(Boolean).join(' '); + if (fieldSchema) { return ( -
    +
    {hasIntersected && ( {fieldSchema.map((field, i) => { @@ -62,14 +70,10 @@ const RenderFields = (props) => { if ((filter && typeof filter === 'function' && filter(field)) || !filter) { const FieldComponent = field?.admin?.hidden ? fieldTypes.hidden : fieldTypes[field.type]; - let initialFieldData; let fieldPermissions = permissions[field.name]; if (!field.name) { - initialFieldData = initialData; fieldPermissions = permissions; - } else if (initialData?.[field.name] !== undefined) { - initialFieldData = initialData[field.name]; } let { admin: { readOnly } = {} } = field; @@ -84,14 +88,13 @@ const RenderFields = (props) => { if (FieldComponent) { return ( { }; RenderFields.defaultProps = { - initialData: {}, customComponentsPath: '', filter: null, readOnly: false, permissions: {}, operation: undefined, + className: undefined, }; RenderFields.propTypes = { fieldSchema: PropTypes.arrayOf( PropTypes.shape({}), ).isRequired, - initialData: PropTypes.shape({}), customComponentsPath: PropTypes.string, fieldTypes: PropTypes.shape({ hidden: PropTypes.function, @@ -153,6 +155,7 @@ RenderFields.propTypes = { permissions: PropTypes.shape({}), readOnly: PropTypes.bool, operation: PropTypes.string, + className: PropTypes.string, }; export default RenderFields; diff --git a/src/client/components/forms/RenderFields/index.scss b/src/client/components/forms/RenderFields/index.scss deleted file mode 100644 index 40b8ea494..000000000 --- a/src/client/components/forms/RenderFields/index.scss +++ /dev/null @@ -1,3 +0,0 @@ -.missing-field { - -} diff --git a/src/client/components/forms/field-types/Array/index.js b/src/client/components/forms/field-types/Array/index.js index f49b2ccb0..5a107e7f8 100644 --- a/src/client/components/forms/field-types/Array/index.js +++ b/src/client/components/forms/field-types/Array/index.js @@ -1,7 +1,6 @@ import React, { useEffect, useReducer, useCallback, useState } from 'react'; import PropTypes from 'prop-types'; import { DragDropContext, Droppable } from 'react-beautiful-dnd'; -import { v4 as uuidv4 } from 'uuid'; import withCondition from '../../withCondition'; import Button from '../../../elements/Button'; @@ -23,8 +22,6 @@ const ArrayFieldType = (props) => { name, path: pathFromProps, fields, - defaultValue, - initialData, fieldTypes, validate, required, @@ -34,10 +31,9 @@ const ArrayFieldType = (props) => { permissions, } = props; - const dataToInitialize = initialData || defaultValue; const [rows, dispatchRows] = useReducer(reducer, []); const { customComponentsPath } = useRenderedFields(); - const { getDataByPath } = useForm(); + const { getDataByPath, initialState, dispatchFields } = useForm(); const path = pathFromProps || name; @@ -57,40 +53,25 @@ const ArrayFieldType = (props) => { path, validate: memoizedValidate, disableFormData, - initialData: initialData?.length, - defaultValue: defaultValue?.length, + ignoreWhileFlattening: true, required, }); const addRow = useCallback((rowIndex) => { - const data = getDataByPath(path); - - dispatchRows({ - type: 'ADD', index: rowIndex, data, - }); - + dispatchRows({ type: 'ADD', rowIndex }); + dispatchFields({ type: 'ADD_ROW', rowIndex, fieldSchema: fields, path }); setValue(value + 1); - }, [dispatchRows, getDataByPath, path, setValue, value]); + }, [dispatchRows, dispatchFields, fields, path, setValue, value]); const removeRow = useCallback((rowIndex) => { - const data = getDataByPath(path); - - dispatchRows({ - type: 'REMOVE', - index: rowIndex, - data, - }); - - setValue(value - 1); - }, [dispatchRows, path, getDataByPath, setValue, value]); + dispatchRows({ type: 'REMOVE', rowIndex }); + dispatchFields({ type: 'REMOVE_ROW', rowIndex, path }); + }, [dispatchRows, dispatchFields, path]); const moveRow = useCallback((moveFromIndex, moveToIndex) => { - const data = getDataByPath(path); - - dispatchRows({ - type: 'MOVE', index: moveFromIndex, moveToIndex, data, - }); - }, [dispatchRows, getDataByPath, path]); + dispatchRows({ type: 'MOVE', moveFromIndex, moveToIndex }); + dispatchFields({ type: 'MOVE_ROW', moveFromIndex, moveToIndex, path }); + }, [dispatchRows, dispatchFields, path]); const onDragEnd = useCallback((result) => { if (!result.destination) return; @@ -100,28 +81,19 @@ const ArrayFieldType = (props) => { }, [moveRow]); useEffect(() => { - dispatchRows({ - type: 'SET_ALL', - rows: dataToInitialize.reduce((acc, data) => ([ - ...acc, - { - key: uuidv4(), - open: true, - data, - }, - ]), []), - }); - }, [dataToInitialize]); + const data = getDataByPath(path); + dispatchRows({ type: 'SET_ALL', data }); + }, [initialState, getDataByPath, path]); useEffect(() => { - if (value === 0 && dataToInitialize.length > 0 && disableFormData) { + setValue(rows?.length || 0); + + if (rows?.length === 0) { setDisableFormData(false); - setValue(value); - } else if (value > 0 && !disableFormData) { + } else { setDisableFormData(true); - setValue(value); } - }, [value, setValue, disableFormData, dataToInitialize]); + }, [rows, setValue]); return ( { ArrayFieldType.defaultProps = { label: '', - defaultValue: [], - initialData: [], validate: array, required: false, maxRows: undefined, @@ -158,12 +128,6 @@ ArrayFieldType.defaultProps = { }; ArrayFieldType.propTypes = { - defaultValue: PropTypes.arrayOf( - PropTypes.shape({}), - ), - initialData: PropTypes.arrayOf( - PropTypes.shape({}), - ), fields: PropTypes.arrayOf( PropTypes.shape({}), ).isRequired, @@ -230,7 +194,6 @@ const RenderArray = React.memo((props) => { removeRow={() => removeRow(i)} moveRow={moveRow} parentPath={path} - initialData={row.data} initNull={row.initNull} customComponentsPath={`${customComponentsPath}${name}.fields.`} fieldTypes={fieldTypes} @@ -259,4 +222,40 @@ const RenderArray = React.memo((props) => { ); }); +RenderArray.defaultProps = { + label: undefined, + showError: false, + errorMessage: undefined, + rows: [], + singularLabel: 'Row', + path: '', + customComponentsPath: undefined, + value: undefined, +}; + +RenderArray.propTypes = { + label: PropTypes.string, + showError: PropTypes.bool, + errorMessage: PropTypes.string, + rows: PropTypes.arrayOf( + PropTypes.shape({}), + ), + singularLabel: PropTypes.string, + path: PropTypes.string, + customComponentsPath: PropTypes.string, + name: PropTypes.string.isRequired, + value: PropTypes.number, + onDragEnd: PropTypes.func.isRequired, + addRow: PropTypes.func.isRequired, + removeRow: PropTypes.func.isRequired, + moveRow: PropTypes.func.isRequired, + fieldTypes: PropTypes.shape({}).isRequired, + fields: PropTypes.arrayOf( + PropTypes.shape({}), + ).isRequired, + permissions: PropTypes.shape({ + fields: PropTypes.shape({}), + }).isRequired, +}; + export default withCondition(ArrayFieldType); diff --git a/src/client/components/forms/field-types/Blocks/BlockSelector/BlockSelection/index.js b/src/client/components/forms/field-types/Blocks/BlockSelector/BlockSelection/index.js index f3236a40f..a4a7a4789 100644 --- a/src/client/components/forms/field-types/Blocks/BlockSelector/BlockSelection/index.js +++ b/src/client/components/forms/field-types/Blocks/BlockSelector/BlockSelection/index.js @@ -22,10 +22,10 @@ const BlockSelection = (props) => { }; return ( -
    @@ -36,11 +36,10 @@ const BlockSelection = (props) => { alt={blockImageAltText} /> ) - : - } + : }
    {labels.singular}
    -
    + ); }; diff --git a/src/client/components/forms/field-types/Blocks/BlockSelector/BlockSelection/index.scss b/src/client/components/forms/field-types/Blocks/BlockSelector/BlockSelection/index.scss index 50b5ae7fe..719a1f545 100644 --- a/src/client/components/forms/field-types/Blocks/BlockSelector/BlockSelection/index.scss +++ b/src/client/components/forms/field-types/Blocks/BlockSelector/BlockSelection/index.scss @@ -8,6 +8,10 @@ padding: base(.75) base(.5); cursor: pointer; align-items: center; + background: none; + border-radius: 0; + box-shadow: 0; + border: 0; &:hover { background-color: $color-background-gray; diff --git a/src/client/components/forms/field-types/Blocks/index.js b/src/client/components/forms/field-types/Blocks/index.js index 716ab4902..2e75b0c43 100644 --- a/src/client/components/forms/field-types/Blocks/index.js +++ b/src/client/components/forms/field-types/Blocks/index.js @@ -3,7 +3,6 @@ import React, { } from 'react'; import PropTypes from 'prop-types'; import { DragDropContext, Droppable } from 'react-beautiful-dnd'; -import { v4 as uuidv4 } from 'uuid'; import withCondition from '../../withCondition'; import Button from '../../../elements/Button'; @@ -27,8 +26,6 @@ const Blocks = (props) => { name, path: pathFromProps, blocks, - defaultValue, - initialData, singularLabel, fieldTypes, maxRows, @@ -40,6 +37,10 @@ const Blocks = (props) => { const path = pathFromProps || name; + const [rows, dispatchRows] = useReducer(reducer, []); + const { customComponentsPath } = useRenderedFields(); + const { getDataByPath, initialState, dispatchFields } = useForm(); + const memoizedValidate = useCallback((value) => { const validationResult = validate( value, @@ -61,51 +62,32 @@ const Blocks = (props) => { path, validate: memoizedValidate, disableFormData, - initialData: initialData?.length, - defaultValue: defaultValue?.length, + ignoreWhileFlattening: true, required, }); - const dataToInitialize = initialData || defaultValue; - const [rows, dispatchRows] = useReducer(reducer, []); - const { customComponentsPath } = useRenderedFields(); - const { getDataByPath } = useForm(); - - const addRow = useCallback((index, blockType) => { - const data = getDataByPath(path); - - dispatchRows({ - type: 'ADD', index, data, initialRowData: { blockType }, - }); + const addRow = useCallback((rowIndex, blockType) => { + const block = blocks.find((potentialBlock) => potentialBlock.slug === blockType); + dispatchRows({ type: 'ADD', rowIndex, blockType }); + dispatchFields({ type: 'ADD_ROW', rowIndex, fieldSchema: block.fields, path, blockType }); setValue(value + 1); - }, [getDataByPath, path, setValue, value]); - - const removeRow = useCallback((index) => { - const data = getDataByPath(path); - - dispatchRows({ - type: 'REMOVE', - index, - data, - }); + }, [path, setValue, value, blocks, dispatchFields]); + const removeRow = useCallback((rowIndex) => { + dispatchRows({ type: 'REMOVE', rowIndex }); + dispatchFields({ type: 'REMOVE_ROW', rowIndex, path }); setValue(value - 1); - }, [getDataByPath, path, setValue, value]); + }, [path, setValue, value, dispatchFields]); const moveRow = useCallback((moveFromIndex, moveToIndex) => { - const data = getDataByPath(path); + dispatchRows({ type: 'MOVE', moveFromIndex, moveToIndex }); + dispatchFields({ type: 'MOVE_ROW', moveFromIndex, moveToIndex, path }); + }, [dispatchRows, dispatchFields, path]); - dispatchRows({ - type: 'MOVE', index: moveFromIndex, moveToIndex, data, - }); - }, [getDataByPath, path]); - - const toggleCollapse = useCallback((index) => { - dispatchRows({ - type: 'TOGGLE_COLLAPSE', index, rows, - }); - }, [rows]); + const toggleCollapse = useCallback((rowIndex) => { + dispatchRows({ type: 'TOGGLE_COLLAPSE', rowIndex }); + }, []); const onDragEnd = useCallback((result) => { if (!result.destination) return; @@ -115,31 +97,22 @@ const Blocks = (props) => { }, [moveRow]); useEffect(() => { - dispatchRows({ - type: 'SET_ALL', - rows: dataToInitialize.reduce((acc, data) => ([ - ...acc, - { - key: uuidv4(), - open: true, - data, - }, - ]), []), - }); - }, [dataToInitialize]); + const data = getDataByPath(path); + dispatchRows({ type: 'SET_ALL', data }); + }, [initialState, getDataByPath, path]); useEffect(() => { - if (value === 0 && dataToInitialize.length > 0 && disableFormData) { + setValue(rows?.length || 0); + + if (rows?.length === 0) { setDisableFormData(false); - setValue(value); - } else if (value > 0 && !disableFormData) { + } else { setDisableFormData(true); - setValue(value); } - }, [value, setValue, disableFormData, dataToInitialize]); + }, [rows, setValue]); return ( - { toggleCollapse={toggleCollapse} permissions={permissions} value={value} - dataToInitialize={dataToInitialize} blocks={blocks} /> ); @@ -164,8 +136,6 @@ const Blocks = (props) => { Blocks.defaultProps = { label: '', - defaultValue: [], - initialData: [], singularLabel: 'Block', validate: blocksValidator, required: false, @@ -178,12 +148,6 @@ Blocks.propTypes = { blocks: PropTypes.arrayOf( PropTypes.shape({}), ).isRequired, - defaultValue: PropTypes.arrayOf( - PropTypes.shape({}), - ), - initialData: PropTypes.arrayOf( - PropTypes.shape({}), - ), label: PropTypes.string, singularLabel: PropTypes.string, name: PropTypes.string.isRequired, @@ -198,7 +162,7 @@ Blocks.propTypes = { }), }; -const RenderBlock = React.memo((props) => { +const RenderBlocks = React.memo((props) => { const { onDragEnd, label, @@ -216,7 +180,6 @@ const RenderBlock = React.memo((props) => { permissions, value, toggleCollapse, - dataToInitialize, blocks, } = props; @@ -239,12 +202,7 @@ const RenderBlock = React.memo((props) => { {...provided.droppableProps} > {rows.length > 0 && rows.map((row, i) => { - let { blockType } = row.data; - - if (!blockType) { - blockType = dataToInitialize?.[i]?.blockType; - } - + const { blockType } = row; const blockToRender = blocks.find((block) => block.slug === blockType); if (blockToRender) { @@ -263,7 +221,6 @@ const RenderBlock = React.memo((props) => { moveRow={moveRow} toggleRowCollapse={() => toggleCollapse(i)} parentPath={path} - initialData={row.data} customComponentsPath={`${customComponentsPath}${name}.fields.`} fieldTypes={fieldTypes} permissions={permissions.fields} @@ -318,4 +275,41 @@ const RenderBlock = React.memo((props) => { ); }); +RenderBlocks.defaultProps = { + label: undefined, + showError: false, + errorMessage: undefined, + rows: [], + singularLabel: 'Row', + path: '', + customComponentsPath: undefined, + value: undefined, +}; + +RenderBlocks.propTypes = { + label: PropTypes.string, + showError: PropTypes.bool, + errorMessage: PropTypes.string, + rows: PropTypes.arrayOf( + PropTypes.shape({}), + ), + singularLabel: PropTypes.string, + path: PropTypes.string, + customComponentsPath: PropTypes.string, + name: PropTypes.string.isRequired, + value: PropTypes.number, + onDragEnd: PropTypes.func.isRequired, + addRow: PropTypes.func.isRequired, + removeRow: PropTypes.func.isRequired, + moveRow: PropTypes.func.isRequired, + fieldTypes: PropTypes.shape({}).isRequired, + permissions: PropTypes.shape({ + fields: PropTypes.shape({}), + }).isRequired, + blocks: PropTypes.arrayOf( + PropTypes.shape({}), + ).isRequired, + toggleCollapse: PropTypes.func.isRequired, +}; + export default withCondition(Blocks); diff --git a/src/client/components/forms/field-types/Checkbox/index.js b/src/client/components/forms/field-types/Checkbox/index.js index af2bc242d..490dc0927 100644 --- a/src/client/components/forms/field-types/Checkbox/index.js +++ b/src/client/components/forms/field-types/Checkbox/index.js @@ -15,8 +15,6 @@ const Checkbox = (props) => { name, path: pathFromProps, required, - defaultValue, - initialData, validate, label, onChange, @@ -43,8 +41,6 @@ const Checkbox = (props) => { } = useFieldType({ path, required, - initialData, - defaultValue, validate: memoizedValidate, disableFormData, }); @@ -98,8 +94,6 @@ Checkbox.defaultProps = { label: null, required: false, admin: {}, - defaultValue: false, - initialData: false, validate: checkbox, path: '', onChange: undefined, @@ -115,8 +109,6 @@ Checkbox.propTypes = { width: PropTypes.string, }), required: PropTypes.bool, - defaultValue: PropTypes.bool, - initialData: PropTypes.bool, validate: PropTypes.func, label: PropTypes.string, onChange: PropTypes.func, diff --git a/src/client/components/forms/field-types/DateTime/index.js b/src/client/components/forms/field-types/DateTime/index.js index d1b07d98e..e6af855c6 100644 --- a/src/client/components/forms/field-types/DateTime/index.js +++ b/src/client/components/forms/field-types/DateTime/index.js @@ -19,8 +19,6 @@ const DateTime = (props) => { path: pathFromProps, name, required, - defaultValue, - initialData, validate, errorMessage, label, @@ -45,8 +43,6 @@ const DateTime = (props) => { } = useFieldType({ path, required, - initialData, - defaultValue, validate: memoizedValidate, }); @@ -88,8 +84,6 @@ const DateTime = (props) => { DateTime.defaultProps = { label: null, required: false, - defaultValue: undefined, - initialData: undefined, validate: date, errorMessage: defaultError, admin: {}, @@ -101,8 +95,6 @@ DateTime.propTypes = { path: PropTypes.string, label: PropTypes.string, required: PropTypes.bool, - defaultValue: PropTypes.string, - initialData: PropTypes.string, validate: PropTypes.func, errorMessage: PropTypes.string, admin: PropTypes.shape({ diff --git a/src/client/components/forms/field-types/Email/index.js b/src/client/components/forms/field-types/Email/index.js index 251648328..ca0fbc2e2 100644 --- a/src/client/components/forms/field-types/Email/index.js +++ b/src/client/components/forms/field-types/Email/index.js @@ -13,8 +13,6 @@ const Email = (props) => { name, path: pathFromProps, required, - defaultValue, - initialData, validate, admin: { readOnly, @@ -35,9 +33,6 @@ const Email = (props) => { const fieldType = useFieldType({ path, - required, - initialData, - defaultValue, validate: memoizedValidate, enableDebouncedValue: true, }); diff --git a/src/client/components/forms/field-types/Group/index.js b/src/client/components/forms/field-types/Group/index.js index 7e6c9fcd1..6421a0e88 100644 --- a/src/client/components/forms/field-types/Group/index.js +++ b/src/client/components/forms/field-types/Group/index.js @@ -7,7 +7,7 @@ import './index.scss'; const Group = (props) => { const { - label, fields, name, path: pathFromProps, fieldTypes, initialData, + label, fields, name, path: pathFromProps, fieldTypes, } = props; const path = pathFromProps || name; @@ -18,7 +18,6 @@ const Group = (props) => {

    {label}

    ({ @@ -32,12 +31,10 @@ const Group = (props) => { Group.defaultProps = { label: '', - initialData: {}, path: '', }; Group.propTypes = { - initialData: PropTypes.shape({}), fields: PropTypes.arrayOf( PropTypes.shape({}), ).isRequired, diff --git a/src/client/components/forms/field-types/HiddenInput/index.js b/src/client/components/forms/field-types/HiddenInput/index.js index 6010187c6..3eed61c58 100644 --- a/src/client/components/forms/field-types/HiddenInput/index.js +++ b/src/client/components/forms/field-types/HiddenInput/index.js @@ -8,8 +8,6 @@ const HiddenInput = (props) => { name, path: pathFromProps, required, - defaultValue, - initialData, } = props; const path = pathFromProps || name; @@ -17,8 +15,6 @@ const HiddenInput = (props) => { const { value, setValue } = useFieldType({ path, required, - initialData, - defaultValue, }); return ( @@ -33,8 +29,6 @@ const HiddenInput = (props) => { HiddenInput.defaultProps = { required: false, - defaultValue: undefined, - initialData: undefined, path: '', }; @@ -42,8 +36,6 @@ HiddenInput.propTypes = { name: PropTypes.string.isRequired, path: PropTypes.string, required: PropTypes.bool, - defaultValue: PropTypes.string, - initialData: PropTypes.string, }; export default withCondition(HiddenInput); diff --git a/src/client/components/forms/field-types/Number/index.js b/src/client/components/forms/field-types/Number/index.js index 848ccac78..b4bc45626 100644 --- a/src/client/components/forms/field-types/Number/index.js +++ b/src/client/components/forms/field-types/Number/index.js @@ -13,8 +13,6 @@ const NumberField = (props) => { name, path: pathFromProps, required, - defaultValue, - initialData, validate, label, placeholder, @@ -41,9 +39,6 @@ const NumberField = (props) => { errorMessage, } = useFieldType({ path, - required, - initialData, - defaultValue, validate: memoizedValidate, enableDebouncedValue: true, }); @@ -95,8 +90,6 @@ NumberField.defaultProps = { label: null, path: undefined, required: false, - defaultValue: undefined, - initialData: undefined, placeholder: undefined, max: undefined, min: undefined, @@ -109,8 +102,6 @@ NumberField.propTypes = { path: PropTypes.string, required: PropTypes.bool, placeholder: PropTypes.string, - defaultValue: PropTypes.number, - initialData: PropTypes.number, validate: PropTypes.func, admin: PropTypes.shape({ readOnly: PropTypes.bool, diff --git a/src/client/components/forms/field-types/Password/index.js b/src/client/components/forms/field-types/Password/index.js index 040ad6520..5664a0120 100644 --- a/src/client/components/forms/field-types/Password/index.js +++ b/src/client/components/forms/field-types/Password/index.js @@ -13,8 +13,6 @@ const Password = (props) => { path: pathFromProps, name, required, - defaultValue, - initialData, validate, style, width, @@ -38,8 +36,6 @@ const Password = (props) => { } = useFieldType({ path, required, - initialData, - defaultValue, validate: memoizedValidate, enableDebouncedValue: true, }); @@ -82,8 +78,6 @@ const Password = (props) => { Password.defaultProps = { required: false, - initialData: undefined, - defaultValue: undefined, validate: password, width: undefined, style: {}, @@ -95,8 +89,6 @@ Password.propTypes = { name: PropTypes.string.isRequired, path: PropTypes.string, required: PropTypes.bool, - initialData: PropTypes.string, - defaultValue: PropTypes.string, width: PropTypes.string, style: PropTypes.shape({}), label: PropTypes.string.isRequired, diff --git a/src/client/components/forms/field-types/RadioGroup/index.js b/src/client/components/forms/field-types/RadioGroup/index.js index c4991e195..50985506c 100644 --- a/src/client/components/forms/field-types/RadioGroup/index.js +++ b/src/client/components/forms/field-types/RadioGroup/index.js @@ -15,8 +15,6 @@ const RadioGroup = (props) => { name, path: pathFromProps, required, - defaultValue, - initialData, validate, label, admin: { @@ -42,8 +40,6 @@ const RadioGroup = (props) => { } = useFieldType({ path, required, - initialData, - defaultValue, validate: memoizedValidate, }); @@ -72,7 +68,7 @@ const RadioGroup = (props) => { required={required} /> {options?.map((option) => { - const isSelected = !value ? (option.value === defaultValue) : (option.value === value); + const isSelected = option.value === value; return ( { RadioGroup.defaultProps = { label: null, required: false, - defaultValue: null, - initialData: undefined, validate: radio, admin: {}, path: '', @@ -101,8 +95,6 @@ RadioGroup.propTypes = { path: PropTypes.string, name: PropTypes.string.isRequired, required: PropTypes.bool, - defaultValue: PropTypes.string, - initialData: PropTypes.string, validate: PropTypes.func, admin: PropTypes.shape({ readOnly: PropTypes.bool, diff --git a/src/client/components/forms/field-types/Relationship/index.js b/src/client/components/forms/field-types/Relationship/index.js index 6a555400a..c33b13aa1 100644 --- a/src/client/components/forms/field-types/Relationship/index.js +++ b/src/client/components/forms/field-types/Relationship/index.js @@ -1,5 +1,5 @@ import React, { - Component, useState, useEffect, useCallback, + Component, useCallback, } from 'react'; import PropTypes from 'prop-types'; import some from 'async-some'; @@ -90,7 +90,7 @@ class Relationship extends Component { error = 'You do not have permission to load options for this field.'; } - this.setState({ + return this.setState({ errorLoading: error, }); }, (lastPage, nextPage) => { @@ -245,10 +245,6 @@ class Relationship extends Component { const valueToRender = this.findValueInOptions(options, value); - // /////////////////////////////////////////// - // TODO: simplify formatValue pattern seen below with react select - // /////////////////////////////////////////// - return (
    { - const [formattedInitialData, setFormattedInitialData] = useState(undefined); - const { - defaultValue, relationTo, hasMany, validate, path, name, initialData, required, + relationTo, validate, path, name, required, } = props; const hasMultipleRelations = Array.isArray(relationTo); - const dataToInitialize = initialData || defaultValue; const memoizedValidate = useCallback((value) => { const validationResult = validate(value, { required }); @@ -348,41 +341,10 @@ const RelationshipFieldType = (props) => { const fieldType = useFieldType({ ...props, path: path || name, - initialData: formattedInitialData, - defaultValue, validate: memoizedValidate, required, }); - useEffect(() => { - const formatInitialData = (valueToFormat) => { - if (hasMultipleRelations) { - const id = valueToFormat?.value?.id || valueToFormat?.value; - - return { - ...valueToFormat, - value: id, - }; - } - - return valueToFormat?.id || valueToFormat; - }; - - if (dataToInitialize) { - if (hasMany && Array.isArray(dataToInitialize)) { - const newFormattedInitialData = []; - - dataToInitialize.forEach((individualValue) => { - newFormattedInitialData.push(formatInitialData(individualValue)); - }); - - setFormattedInitialData(newFormattedInitialData); - } else { - setFormattedInitialData(formatInitialData(dataToInitialize)); - } - } - }, [dataToInitialize, hasMany, hasMultipleRelations]); - return ( { path: pathFromProps, name, required, - defaultValue, - initialData, validate, label, placeholder, @@ -93,8 +91,6 @@ const RichText = (props) => { const fieldType = useFieldType({ path, required, - initialData, - defaultValue, validate, }); @@ -164,8 +160,6 @@ const RichText = (props) => { RichText.defaultProps = { label: null, required: false, - defaultValue: undefined, - initialData: undefined, placeholder: undefined, admin: {}, validate: richText, @@ -177,8 +171,6 @@ RichText.propTypes = { path: PropTypes.string, required: PropTypes.bool, placeholder: PropTypes.string, - defaultValue: PropTypes.string, - initialData: PropTypes.arrayOf(PropTypes.shape({})), validate: PropTypes.func, admin: PropTypes.shape({ readOnly: PropTypes.bool, diff --git a/src/client/components/forms/field-types/Row/index.js b/src/client/components/forms/field-types/Row/index.js index 13646dea0..144349a17 100644 --- a/src/client/components/forms/field-types/Row/index.js +++ b/src/client/components/forms/field-types/Row/index.js @@ -7,29 +7,24 @@ import './index.scss'; const Row = (props) => { const { - fields, fieldTypes, initialData, path, permissions, + fields, fieldTypes, path, permissions, } = props; return ( -
    - { - return { - ...field, - path: `${path ? `${path}.` : ''}${field.name}`, - }; - })} - /> -
    + ({ + ...field, + path: `${path ? `${path}.` : ''}${field.name}`, + }))} + /> ); }; Row.defaultProps = { path: '', - initialData: undefined, permissions: {}, }; @@ -39,7 +34,6 @@ Row.propTypes = { ).isRequired, fieldTypes: PropTypes.shape({}).isRequired, path: PropTypes.string, - initialData: PropTypes.shape({}), permissions: PropTypes.shape({}), }; diff --git a/src/client/components/forms/field-types/Select/index.js b/src/client/components/forms/field-types/Select/index.js index 65d2de147..7b9029a4f 100644 --- a/src/client/components/forms/field-types/Select/index.js +++ b/src/client/components/forms/field-types/Select/index.js @@ -65,8 +65,6 @@ const Select = (props) => { path: pathFromProps, name, required, - defaultValue, - initialData, validate, label, options, @@ -94,8 +92,6 @@ const Select = (props) => { path, label, required, - initialData, - defaultValue, validate: memoizedValidate, }); @@ -142,8 +138,6 @@ Select.defaultProps = { admin: {}, required: false, validate: select, - defaultValue: undefined, - initialData: undefined, hasMany: false, path: '', }; @@ -156,14 +150,6 @@ Select.propTypes = { width: PropTypes.string, }), label: PropTypes.string.isRequired, - defaultValue: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.array, - ]), - initialData: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.array, - ]), validate: PropTypes.func, name: PropTypes.string.isRequired, path: PropTypes.string, diff --git a/src/client/components/forms/field-types/Text/index.js b/src/client/components/forms/field-types/Text/index.js index 122c889a1..69dd6f036 100644 --- a/src/client/components/forms/field-types/Text/index.js +++ b/src/client/components/forms/field-types/Text/index.js @@ -13,8 +13,6 @@ const Text = (props) => { path: pathFromProps, name, required, - defaultValue, - initialData, validate, label, placeholder, @@ -36,9 +34,6 @@ const Text = (props) => { const fieldType = useFieldType({ path, - required, - initialData, - defaultValue, validate: memoizedValidate, enableDebouncedValue: true, }); @@ -91,8 +86,6 @@ Text.defaultProps = { label: null, required: false, admin: {}, - defaultValue: undefined, - initialData: undefined, placeholder: undefined, validate: text, path: '', @@ -105,8 +98,6 @@ Text.propTypes = { path: PropTypes.string, required: PropTypes.bool, placeholder: PropTypes.string, - defaultValue: PropTypes.string, - initialData: PropTypes.string, validate: PropTypes.func, admin: PropTypes.shape({ readOnly: PropTypes.bool, diff --git a/src/client/components/forms/field-types/Textarea/index.js b/src/client/components/forms/field-types/Textarea/index.js index 160584e6f..793108e62 100644 --- a/src/client/components/forms/field-types/Textarea/index.js +++ b/src/client/components/forms/field-types/Textarea/index.js @@ -13,14 +13,14 @@ const Textarea = (props) => { path: pathFromProps, name, required, - defaultValue, - initialData, validate, - style, - width, + admin: { + readOnly, + style, + width, + } = {}, label, placeholder, - readOnly, minLength, maxLength, rows, @@ -40,9 +40,6 @@ const Textarea = (props) => { errorMessage, } = useFieldType({ path, - required, - initialData, - defaultValue, validate: memoizedValidate, enableDebouncedValue: true, }); @@ -87,8 +84,6 @@ const Textarea = (props) => { Textarea.defaultProps = { required: false, label: null, - defaultValue: undefined, - initialData: undefined, validate: textarea, placeholder: null, path: '', @@ -102,8 +97,6 @@ Textarea.propTypes = { name: PropTypes.string.isRequired, path: PropTypes.string, required: PropTypes.bool, - defaultValue: PropTypes.string, - initialData: PropTypes.string, validate: PropTypes.func, admin: PropTypes.shape({ readOnly: PropTypes.bool, diff --git a/src/client/components/forms/field-types/Upload/index.js b/src/client/components/forms/field-types/Upload/index.js index ddaf40112..3ce9c0f41 100644 --- a/src/client/components/forms/field-types/Upload/index.js +++ b/src/client/components/forms/field-types/Upload/index.js @@ -26,8 +26,6 @@ const Upload = (props) => { path: pathFromProps, name, required, - defaultValue, - initialData, admin: { readOnly, style, @@ -45,8 +43,6 @@ const Upload = (props) => { const addModalSlug = `${path}-add`; const selectExistingModalSlug = `${path}-select-existing`; - const dataToInitialize = (typeof initialData === 'object' && initialData.id) ? initialData.id : initialData; - const memoizedValidate = useCallback((value) => { const validationResult = validate(value, { required }); return validationResult; @@ -55,8 +51,6 @@ const Upload = (props) => { const fieldType = useFieldType({ path, required, - initialData: dataToInitialize, - defaultValue, validate: memoizedValidate, }); @@ -75,13 +69,9 @@ const Upload = (props) => { ].filter(Boolean).join(' '); useEffect(() => { - if (typeof initialData === 'object' && initialData?.id) { - setInternalValue(initialData); - } - - if (typeof initialData === 'string') { + if (typeof value === 'string') { const fetchFile = async () => { - const response = await fetch(`${serverURL}${api}/${relationTo}/${initialData}`); + const response = await fetch(`${serverURL}${api}/${relationTo}/${value}`); if (response.ok) { const json = await response.json(); @@ -91,7 +81,7 @@ const Upload = (props) => { fetchFile(); } - }, [initialData, setInternalValue, relationTo]); + }, [value, setInternalValue, relationTo]); return (
    { Upload.defaultProps = { label: null, required: false, - defaultValue: undefined, - initialData: undefined, admin: {}, validate: upload, path: '', @@ -184,13 +172,6 @@ Upload.propTypes = { name: PropTypes.string.isRequired, path: PropTypes.string, required: PropTypes.bool, - defaultValue: PropTypes.string, - initialData: PropTypes.oneOfType([ - PropTypes.shape({ - id: PropTypes.string, - }), - PropTypes.string, - ]), validate: PropTypes.func, admin: PropTypes.shape({ readOnly: PropTypes.bool, diff --git a/src/client/components/forms/field-types/index.js b/src/client/components/forms/field-types/index.js index fc431cb59..c9aa948a5 100644 --- a/src/client/components/forms/field-types/index.js +++ b/src/client/components/forms/field-types/index.js @@ -1,6 +1,3 @@ -export { default as auth } from './Auth'; -export { default as file } from './File'; - export { default as email } from './Email'; export { default as hidden } from './HiddenInput'; export { default as text } from './Text'; diff --git a/src/client/components/forms/field-types/rowReducer.js b/src/client/components/forms/field-types/rowReducer.js index e21a5103f..8f78018f7 100644 --- a/src/client/components/forms/field-types/rowReducer.js +++ b/src/client/components/forms/field-types/rowReducer.js @@ -2,59 +2,57 @@ import { v4 as uuidv4 } from 'uuid'; const reducer = (currentState, action) => { const { - type, index, moveToIndex, rows, data = [], initialRowData = {}, + type, rowIndex, moveFromIndex, moveToIndex, data, blockType, } = action; const stateCopy = [...currentState]; switch (type) { - case 'SET_ALL': - return rows; + case 'SET_ALL': { + if (Array.isArray(data)) { + return data.map((dataRow) => { + const row = { + key: uuidv4(), + open: true, + }; + + if (dataRow.blockType) { + row.blockType = dataRow.blockType; + } + + return row; + }); + } + + return []; + } case 'TOGGLE_COLLAPSE': - stateCopy[index].open = !stateCopy[index].open; + stateCopy[rowIndex].open = !stateCopy[rowIndex].open; return stateCopy; case 'ADD': { - stateCopy.splice(index + 1, 0, { + const newRow = { open: true, key: uuidv4(), - data, - }); + }; - data.splice(index + 1, 0, initialRowData); + if (blockType) newRow.blockType = blockType; - const result = stateCopy.map((row, i) => { - return { - ...row, - data: { - ...(data[i] || {}), - }, - }; - }); + stateCopy.splice(rowIndex + 1, 0, newRow); - return result; + return stateCopy; } - case 'REMOVE': - stateCopy.splice(index, 1); + stateCopy.splice(rowIndex, 1); return stateCopy; case 'MOVE': { - const stateCopyWithNewData = stateCopy.map((row, i) => { - return { - ...row, - data: { - ...(data[i] || {}), - }, - }; - }); - - const movingRowState = { ...stateCopyWithNewData[index] }; - stateCopyWithNewData.splice(index, 1); - stateCopyWithNewData.splice(moveToIndex, 0, movingRowState); - return stateCopyWithNewData; + const movingRowState = { ...stateCopy[moveFromIndex] }; + stateCopy.splice(moveFromIndex, 1); + stateCopy.splice(moveToIndex, 0, movingRowState); + return stateCopy; } default: diff --git a/src/client/components/forms/useFieldType/index.js b/src/client/components/forms/useFieldType/index.js index afc8f0296..a3ca4efef 100644 --- a/src/client/components/forms/useFieldType/index.js +++ b/src/client/components/forms/useFieldType/index.js @@ -3,43 +3,36 @@ import { } from 'react'; import { useFormProcessing, useFormSubmitted, useFormModified, useForm } from '../Form/context'; import useDebounce from '../../../hooks/useDebounce'; -import useUnmountEffect from '../../../hooks/useUnmountEffect'; import './index.scss'; const useFieldType = (options) => { const { path, - initialData: data, - defaultValue, validate, - disableFormData, enableDebouncedValue, + disableFormData, + ignoreWhileFlattening, } = options; - // Determine what the initial data is to be used - // If initialData is defined, that means that data has been provided - // via the API and should override any default values present. - // If no initialData, use default value - const initialData = data !== undefined ? data : defaultValue; - const formContext = useForm(); const submitted = useFormSubmitted(); const processing = useFormProcessing(); const modified = useFormModified(); const { - dispatchFields, getField, setModified, + dispatchFields, getField, setModified, reset, } = formContext; - const [internalValue, setInternalValue] = useState(initialData); + const [internalValue, setInternalValue] = useState(undefined); // Debounce internal values to update form state only every 60ms const debouncedValue = useDebounce(internalValue, 120); // Get field by path const field = getField(path); - const fieldExists = Boolean(field); + + const initialValue = field?.initialValue; // Valid could be a string equal to an error message const valid = (field && typeof field.valid === 'boolean') ? field.valid : true; @@ -57,48 +50,39 @@ const useFieldType = (options) => { fieldToDispatch.valid = false; } - if (disableFormData) { - fieldToDispatch.disableFormData = true; - } + fieldToDispatch.disableFormData = disableFormData; + fieldToDispatch.ignoreWhileFlattening = ignoreWhileFlattening; + fieldToDispatch.initialValue = initialValue; dispatchFields(fieldToDispatch); - }, [path, dispatchFields, validate, disableFormData]); - + }, [path, dispatchFields, validate, disableFormData, ignoreWhileFlattening, initialValue]); // Method to return from `useFieldType`, used to // update internal field values from field component(s) // as fast as they arrive. NOTE - this method is NOT debounced const setValue = useCallback((e) => { - const value = (e && e.target) ? e.target.value : e; + const val = (e && e.target) ? e.target.value : e; if (!modified) setModified(true); - setInternalValue(value); + setInternalValue(val); }, [setModified, modified]); - // Remove field from state on "unmount" - // This is mostly used for array / flex content row modifications - useUnmountEffect(() => { - formContext.dispatchFields({ path, type: 'REMOVE' }); - }); + useEffect(() => { + setInternalValue(initialValue); + }, [initialValue]); // The only time that the FORM value should be updated // is when the debounced value updates. So, when the debounced value updates, // send it up to the form - const formValue = enableDebouncedValue ? debouncedValue : internalValue; + const valueToSend = enableDebouncedValue ? debouncedValue : internalValue; useEffect(() => { - if (!fieldExists || formValue !== undefined) { - sendField(formValue); + if (valueToSend !== undefined) { + sendField(valueToSend); } - }, [formValue, sendField, fieldExists]); - - useEffect(() => { - if (initialData !== undefined) { - setInternalValue(initialData); - } - }, [initialData]); + }, [valueToSend, sendField]); return { ...options, diff --git a/src/client/components/views/Global/Default.js b/src/client/components/views/Global/Default.js index 50da8b2b3..7a7c5b18f 100644 --- a/src/client/components/views/Global/Default.js +++ b/src/client/components/views/Global/Default.js @@ -1,7 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; import format from 'date-fns/format'; -import config from 'payload/config'; import Eyebrow from '../../elements/Eyebrow'; import Form from '../../forms/Form'; import PreviewButton from '../../elements/PreviewButton'; @@ -13,13 +12,11 @@ import LeaveWithoutSaving from '../../modals/LeaveWithoutSaving'; import './index.scss'; -const { serverURL, routes: { api } } = config; - const baseClass = 'global-edit'; const DefaultGlobalView = (props) => { const { - global, data, onSave, permissions, + global, data, onSave, permissions, action, apiURL, } = props; const { @@ -29,8 +26,6 @@ const DefaultGlobalView = (props) => { label, } = global; - const apiURL = `${serverURL}${api}/globals/${slug}`; - const action = `${serverURL}${api}/globals/${slug}`; const hasSavePermission = permissions?.update?.permission; return ( @@ -57,7 +52,7 @@ const DefaultGlobalView = (props) => { operation="update" readOnly={!hasSavePermission} permissions={permissions.fields} - filter={field => (!field.position || (field.position && field.position !== 'sidebar'))} + filter={(field) => (!field.position || (field.position && field.position !== 'sidebar'))} fieldTypes={fieldTypes} fieldSchema={fields} initialData={data} @@ -93,7 +88,7 @@ const DefaultGlobalView = (props) => { operation="update" readOnly={!hasSavePermission} permissions={permissions.fields} - filter={field => field.position === 'sidebar'} + filter={(field) => field.position === 'sidebar'} position="sidebar" fieldTypes={fieldTypes} fieldSchema={fields} @@ -138,6 +133,8 @@ DefaultGlobalView.propTypes = { }), fields: PropTypes.shape({}), }).isRequired, + action: PropTypes.string.isRequired, + apiURL: PropTypes.string.isRequired, }; export default DefaultGlobalView; diff --git a/src/client/components/views/Global/index.js b/src/client/components/views/Global/index.js index 73a1bcdff..047fac239 100644 --- a/src/client/components/views/Global/index.js +++ b/src/client/components/views/Global/index.js @@ -5,6 +5,7 @@ import config from 'payload/config'; import { useStepNav } from '../../elements/StepNav'; import usePayloadAPI from '../../../hooks/usePayloadAPI'; import { useUser } from '../../data/User'; +import { useLocale } from '../../utilities/Locale'; import RenderCustomComponent from '../../utilities/RenderCustomComponent'; import DefaultGlobal from './Default'; @@ -14,6 +15,7 @@ const { serverURL, routes: { admin, api } } = config; const GlobalView = (props) => { const { state: locationState } = useLocation(); const history = useHistory(); + const locale = useLocale(); const { setStepNav } = useStepNav(); const { permissions } = useUser(); @@ -60,6 +62,8 @@ const GlobalView = (props) => { permissions: globalPermissions, global, onSave, + apiURL: `${serverURL}${api}/globals/${slug}?depth=0`, + action: `${serverURL}${api}/globals/${slug}?locale=${locale}`, }} /> ); diff --git a/src/client/components/forms/field-types/Auth/APIKey.js b/src/client/components/views/collections/Edit/Auth/APIKey.js similarity index 71% rename from src/client/components/forms/field-types/Auth/APIKey.js rename to src/client/components/views/collections/Edit/Auth/APIKey.js index 706740a9b..152f71fa4 100644 --- a/src/client/components/forms/field-types/Auth/APIKey.js +++ b/src/client/components/views/collections/Edit/Auth/APIKey.js @@ -1,12 +1,11 @@ import React, { useMemo, useState, useEffect } from 'react'; -import PropTypes from 'prop-types'; import { v4 as uuidv4 } from 'uuid'; -import useFieldType from '../../useFieldType'; -import Label from '../../Label'; -import Button from '../../../elements/Button'; -import CopyToClipboard from '../../../elements/CopyToClipboard'; -import { text } from '../../../../../fields/validations'; -import { useFormFields } from '../../Form/context'; +import useFieldType from '../../../../forms/useFieldType'; +import Label from '../../../../forms/Label'; +import Button from '../../../../elements/Button'; +import CopyToClipboard from '../../../../elements/CopyToClipboard'; +import { text } from '../../../../../../fields/validations'; +import { useFormFields } from '../../../../forms/Form/context'; import './index.scss'; @@ -14,11 +13,7 @@ const path = 'apiKey'; const baseClass = 'api-key'; const validate = (val) => text(val, { minLength: 24, maxLength: 48 }); -const APIKey = (props) => { - const { - initialData, - } = props; - +const APIKey = () => { const [initialAPIKey, setInitialAPIKey] = useState(null); const { getField } = useFormFields(); @@ -38,7 +33,6 @@ const APIKey = (props) => { const fieldType = useFieldType({ path: 'apiKey', - initialData: initialData || initialAPIKey, validate, }); @@ -51,6 +45,12 @@ const APIKey = (props) => { setInitialAPIKey(uuidv4()); }, []); + useEffect(() => { + if (!apiKeyValue) { + setValue(initialAPIKey); + } + }, [apiKeyValue, setValue, initialAPIKey]); + const classes = [ 'field-type', 'api-key', @@ -83,12 +83,4 @@ const APIKey = (props) => { ); }; -APIKey.defaultProps = { - initialData: undefined, -}; - -APIKey.propTypes = { - initialData: PropTypes.string, -}; - export default APIKey; diff --git a/src/client/components/forms/field-types/Auth/index.js b/src/client/components/views/collections/Edit/Auth/index.js similarity index 70% rename from src/client/components/forms/field-types/Auth/index.js rename to src/client/components/views/collections/Edit/Auth/index.js index 2a6062bea..c44897f0c 100644 --- a/src/client/components/forms/field-types/Auth/index.js +++ b/src/client/components/views/collections/Edit/Auth/index.js @@ -1,11 +1,11 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import PropTypes from 'prop-types'; -import Email from '../Email'; -import Password from '../Password'; -import Checkbox from '../Checkbox'; -import Button from '../../../elements/Button'; -import ConfirmPassword from '../ConfirmPassword'; -import { useFormFields } from '../../Form/context'; +import Email from '../../../../forms/field-types/Email'; +import Password from '../../../../forms/field-types/Password'; +import Checkbox from '../../../../forms/field-types/Checkbox'; +import Button from '../../../../elements/Button'; +import ConfirmPassword from '../../../../forms/field-types/ConfirmPassword'; +import { useFormFields, useFormModified } from '../../../../forms/Form/context'; import APIKey from './APIKey'; import './index.scss'; @@ -13,19 +13,25 @@ import './index.scss'; const baseClass = 'auth-fields'; const Auth = (props) => { - const { initialData, useAPIKey, requirePassword } = props; + const { useAPIKey, requirePassword } = props; const [changingPassword, setChangingPassword] = useState(requirePassword); const { getField } = useFormFields(); + const modified = useFormModified(); const enableAPIKey = getField('enableAPIKey'); + useEffect(() => { + if (!modified) { + setChangingPassword(false); + } + }, [modified]); + return (
    {changingPassword && ( @@ -60,12 +66,11 @@ const Auth = (props) => { {useAPIKey && (
    {enableAPIKey?.value && ( - + )}
    )} @@ -74,18 +79,11 @@ const Auth = (props) => { }; Auth.defaultProps = { - initialData: undefined, useAPIKey: false, requirePassword: false, }; Auth.propTypes = { - fieldTypes: PropTypes.shape({}).isRequired, - initialData: PropTypes.shape({ - enableAPIKey: PropTypes.bool, - apiKey: PropTypes.string, - email: PropTypes.string, - }), useAPIKey: PropTypes.bool, requirePassword: PropTypes.bool, }; diff --git a/src/client/components/forms/field-types/Auth/index.scss b/src/client/components/views/collections/Edit/Auth/index.scss similarity index 73% rename from src/client/components/forms/field-types/Auth/index.scss rename to src/client/components/views/collections/Edit/Auth/index.scss index b73c57131..58f522437 100644 --- a/src/client/components/forms/field-types/Auth/index.scss +++ b/src/client/components/views/collections/Edit/Auth/index.scss @@ -1,5 +1,5 @@ -@import '../../../../scss/styles.scss'; -@import '../shared.scss'; +@import '../../../../../scss/styles.scss'; +@import '../../../../forms/field-types/shared.scss'; .auth-fields { margin: base(1.5) 0 base(2); diff --git a/src/client/components/views/collections/Edit/Default.js b/src/client/components/views/collections/Edit/Default.js index e2dcecb77..4234936ef 100644 --- a/src/client/components/views/collections/Edit/Default.js +++ b/src/client/components/views/collections/Edit/Default.js @@ -15,10 +15,12 @@ import DeleteDocument from '../../../elements/DeleteDocument'; import * as fieldTypes from '../../../forms/field-types'; import RenderTitle from '../../../elements/RenderTitle'; import LeaveWithoutSaving from '../../../modals/LeaveWithoutSaving'; +import Auth from './Auth'; +import Upload from './Upload'; import './index.scss'; -const { serverURL, routes: { api, admin } } = config; +const { routes: { admin } } = config; const baseClass = 'collection-edit'; @@ -26,7 +28,16 @@ const DefaultEditView = (props) => { const { params: { id } = {} } = useRouteMatch(); const { - collection, isEditing, data, onSave, permissions, isLoading, + collection, + isEditing, + data, + onSave, + permissions, + isLoading, + initialState, + apiURL, + action, + hasSavePermission, } = props; const { @@ -38,16 +49,9 @@ const DefaultEditView = (props) => { timestamps, preview, auth, + upload, } = collection; - const apiURL = `${serverURL}${api}/${slug}/${id}`; - let action = `${serverURL}${api}/${slug}${isEditing ? `/${id}` : ''}`; - const hasSavePermission = (isEditing && permissions?.update?.permission) || (!isEditing && permissions?.create?.permission); - - if (auth && !isEditing) { - action = `${action}/register`; - } - const classes = [ baseClass, isEditing && `${baseClass}--is-editing`, @@ -61,6 +65,7 @@ const DefaultEditView = (props) => { action={action} onSuccess={onSave} disabled={!hasSavePermission} + initialState={initialState} >
    @@ -76,6 +81,19 @@ const DefaultEditView = (props) => { + {auth && ( + + )} + {upload && ( + + )} { filter={(field) => (!field?.admin?.position || (field?.admin?.position !== 'sidebar'))} fieldTypes={fieldTypes} fieldSchema={fields} - initialData={data} customComponentsPath={`${slug}.fields.`} /> @@ -145,7 +162,6 @@ const DefaultEditView = (props) => { position="sidebar" fieldTypes={fieldTypes} fieldSchema={fields} - initialData={data} customComponentsPath={`${slug}.fields.`} />
    @@ -187,9 +203,14 @@ DefaultEditView.defaultProps = { isEditing: false, isLoading: true, data: undefined, + initialState: undefined, + apiURL: undefined, }; DefaultEditView.propTypes = { + hasSavePermission: PropTypes.bool.isRequired, + action: PropTypes.string.isRequired, + apiURL: PropTypes.string, isLoading: PropTypes.bool, collection: PropTypes.shape({ labels: PropTypes.shape({ @@ -203,7 +224,10 @@ DefaultEditView.propTypes = { fields: PropTypes.arrayOf(PropTypes.shape({})), preview: PropTypes.func, timestamps: PropTypes.bool, - auth: PropTypes.shape({}), + auth: PropTypes.shape({ + useAPIKey: PropTypes.bool, + }), + upload: PropTypes.shape({}), }).isRequired, isEditing: PropTypes.bool, data: PropTypes.shape({ @@ -223,6 +247,7 @@ DefaultEditView.propTypes = { }), fields: PropTypes.shape({}), }).isRequired, + initialState: PropTypes.shape({}), }; export default DefaultEditView; diff --git a/src/client/components/forms/field-types/File/index.js b/src/client/components/views/collections/Edit/Upload/index.js similarity index 92% rename from src/client/components/forms/field-types/File/index.js rename to src/client/components/views/collections/Edit/Upload/index.js index 19565a33d..5ad61a859 100644 --- a/src/client/components/forms/field-types/File/index.js +++ b/src/client/components/views/collections/Edit/Upload/index.js @@ -2,10 +2,10 @@ import React, { useState, useRef, useEffect, useCallback, } from 'react'; import PropTypes from 'prop-types'; -import useFieldType from '../../useFieldType'; -import Button from '../../../elements/Button'; -import FileDetails from '../../../elements/FileDetails'; -import Error from '../../Error'; +import useFieldType from '../../../../forms/useFieldType'; +import Button from '../../../../elements/Button'; +import FileDetails from '../../../../elements/FileDetails'; +import Error from '../../../../forms/Error'; import './index.scss'; @@ -34,10 +34,10 @@ const File = (props) => { const [replacingFile, setReplacingFile] = useState(false); const { - initialData = {}, adminThumbnail, staticURL, + data = {}, adminThumbnail, staticURL, } = props; - const { filename } = initialData; + const { filename } = data; const { value, @@ -122,7 +122,7 @@ const File = (props) => { useEffect(() => { setReplacingFile(false); - }, [initialData]); + }, [data]); const classes = [ baseClass, @@ -138,7 +138,7 @@ const File = (props) => { /> {(filename && !replacingFile) && ( { @@ -167,7 +167,7 @@ const File = (props) => {
    )} {!value && ( - <> +
    { or drag and drop a file here
    - +
    )} { }; File.defaultProps = { - initialData: undefined, + data: undefined, adminThumbnail: undefined, }; File.propTypes = { fieldTypes: PropTypes.shape({}).isRequired, - initialData: PropTypes.shape({ + data: PropTypes.shape({ filename: PropTypes.string, mimeType: PropTypes.string, filesize: PropTypes.number, diff --git a/src/client/components/forms/field-types/File/index.scss b/src/client/components/views/collections/Edit/Upload/index.scss similarity index 96% rename from src/client/components/forms/field-types/File/index.scss rename to src/client/components/views/collections/Edit/Upload/index.scss index ee459b99d..e4b69c186 100644 --- a/src/client/components/forms/field-types/File/index.scss +++ b/src/client/components/views/collections/Edit/Upload/index.scss @@ -1,4 +1,4 @@ -@import '../../../../scss/styles.scss'; +@import '../../../../../scss/styles.scss'; .file-field { position: relative; diff --git a/src/client/components/views/collections/Edit/index.js b/src/client/components/views/collections/Edit/index.js index 2802c3653..558fb1c57 100644 --- a/src/client/components/views/collections/Edit/index.js +++ b/src/client/components/views/collections/Edit/index.js @@ -5,21 +5,14 @@ import config from 'payload/config'; import { useStepNav } from '../../../elements/StepNav'; import usePayloadAPI from '../../../../hooks/usePayloadAPI'; import { useUser } from '../../../data/User'; -import formatFields from './formatFields'; import RenderCustomComponent from '../../../utilities/RenderCustomComponent'; import DefaultEdit from './Default'; +import buildStateFromSchema from '../../../forms/Form/buildStateFromSchema'; const { serverURL, routes: { admin, api } } = config; const EditView = (props) => { - const { params: { id } = {} } = useRouteMatch(); - const { state: locationState } = useLocation(); - const history = useHistory(); - const { setStepNav } = useStepNav(); - const [fields, setFields] = useState([]); - const { permissions } = useUser(); - const { collection, isEditing } = props; const { @@ -30,8 +23,17 @@ const EditView = (props) => { admin: { useAsTitle, }, + fields, + auth, } = collection; + const { params: { id } = {} } = useRouteMatch(); + const { state: locationState } = useLocation(); + const history = useHistory(); + const { setStepNav } = useStepNav(); + const [initialState, setInitialState] = useState({}); + const { permissions } = useUser(); + const onSave = (json) => { history.push(`${admin}/collections/${collection.slug}/${json?.doc?.id}`, { status: { @@ -69,11 +71,24 @@ const EditView = (props) => { }, [setStepNav, isEditing, pluralLabel, dataToRender, slug, useAsTitle]); useEffect(() => { - setFields(formatFields(collection, isEditing)); - }, [collection, isEditing]); + const awaitInitialState = async () => { + const state = await buildStateFromSchema(fields, dataToRender); + setInitialState(state); + }; + + awaitInitialState(); + }, [dataToRender, fields]); const collectionPermissions = permissions?.[slug]; + const apiURL = `${serverURL}${api}/${slug}/${id}`; + let action = `${serverURL}${api}/${slug}${isEditing ? `/${id}` : ''}?depth=0`; + const hasSavePermission = (isEditing && collectionPermissions?.update?.permission) || (!isEditing && collectionPermissions?.create?.permission); + + if (auth && !isEditing) { + action = `${action}/register`; + } + return ( { componentProps={{ isLoading, data: dataToRender, - collection: { ...collection, fields }, + collection, permissions: collectionPermissions, isEditing, onSave, + initialState, + hasSavePermission, + apiURL, + action, }} /> ); @@ -106,6 +125,7 @@ EditView.propTypes = { }), fields: PropTypes.arrayOf(PropTypes.shape({})), preview: PropTypes.func, + auth: PropTypes.shape({}), }).isRequired, isEditing: PropTypes.bool, }; diff --git a/src/client/components/views/collections/List/Default.js b/src/client/components/views/collections/List/Default.js index c2eb2e2b4..1039a4624 100644 --- a/src/client/components/views/collections/List/Default.js +++ b/src/client/components/views/collections/List/Default.js @@ -47,7 +47,9 @@ const DefaultList = (props) => { const [{ data }, { setParams }] = usePayloadAPI(apiURL, { initialParams: { depth: 0 } }); useEffect(() => { - const params = {}; + const params = { + depth: 0, + }; if (page) params.page = page; if (sort) params.sort = sort; From 790c85df5a11a53d6f9e9b1c5d85240e511c2347 Mon Sep 17 00:00:00 2001 From: James Date: Mon, 27 Jul 2020 12:01:14 -0400 Subject: [PATCH 124/125] 0.0.28 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 681408e9e..dd79ab940 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@payloadcms/payload", - "version": "0.0.27", + "version": "0.0.28", "description": "CMS and Application Framework", "license": "ISC", "author": "Payload CMS LLC", From 4d99594021ebfc36359858d638c8dcce6b73f5da Mon Sep 17 00:00:00 2001 From: James Date: Mon, 27 Jul 2020 12:31:28 -0400 Subject: [PATCH 125/125] temporarily re-adds styled components as dependency --- package.json | 1 + yarn.lock | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index dd79ab940..532e66626 100644 --- a/package.json +++ b/package.json @@ -106,6 +106,7 @@ "slate-hyperscript": "^0.58.3", "slate-react": "^0.58.3", "style-loader": "^0.21.0", + "styled-components": "^5.1.1", "uglifyjs-webpack-plugin": "^2.2.0", "url-loader": "^1.0.1", "uuid": "^8.1.0", diff --git a/yarn.lock b/yarn.lock index bfcb75b5d..05e294e95 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9309,7 +9309,7 @@ react-popper@^1.3.4: typed-styles "^0.0.7" warning "^4.0.2" -react-redux@^7.1.1, react-redux@^7.2.0: +react-redux@^7.1.1: version "7.2.0" resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.0.tgz#f970f62192b3981642fec46fd0db18a074fe879d" integrity sha512-EvCAZYGfOLqwV7gh849xy9/pt55rJXPwmYvI4lilPM5rUT/1NxuuN59ipdBksRVSvz0KInbPnp4IfoXJXCqiDA==