From a831add733d5622eeaba5b33493f8b09527002c2 Mon Sep 17 00:00:00 2001 From: brennj Date: Thu, 29 Jun 2023 16:24:04 +0200 Subject: [PATCH 01/69] chore: barebones playground --- .eslintrc | 1 + package-lock.json | 1267 ++++++++++++++++++++++++++++++------- package.json | 13 +- playground/index.html | 13 + playground/src/App.jsx | 7 + playground/src/main.jsx | 9 + playground/vite.config.js | 7 + 7 files changed, 1084 insertions(+), 233 deletions(-) create mode 100644 playground/index.html create mode 100644 playground/src/App.jsx create mode 100644 playground/src/main.jsx create mode 100644 playground/vite.config.js diff --git a/.eslintrc b/.eslintrc index 91703edfe..92d0fcf2a 100644 --- a/.eslintrc +++ b/.eslintrc @@ -2,6 +2,7 @@ "root": true, "extends": ["eslint:recommended", "prettier"], "plugins": ["import", "jest", "sort-keys", "unused-imports"], + "ignorePatterns": ["playground/**"], "parser": "babel-eslint", "env": { "es6": true, diff --git a/package-lock.json b/package-lock.json index cbda87f2b..e4254ec70 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.4.1-beta.0", "license": "MIT", "dependencies": { + "json-logic-js": "^2.0.2", "lodash": "^4.17.21", "randexp": "^0.5.3", "yup": "^0.30.0" @@ -16,6 +17,9 @@ "devDependencies": { "@babel/core": "^7.21.5", "@babel/preset-env": "^7.21.5", + "@types/react": "^18.0.37", + "@types/react-dom": "^18.0.11", + "@vitejs/plugin-react": "^4.0.0", "babel-eslint": "^10.1.0", "babel-jest": "^29.5.0", "child-process-promise": "^2.2.1", @@ -25,6 +29,9 @@ "eslint-plugin-import": "^2.27.5", "eslint-plugin-jest": "^27.2.1", "eslint-plugin-prettier": "^4.2.1", + "eslint-plugin-react": "^7.32.2", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.3.4", "eslint-plugin-sort-keys": "^2.3.5", "eslint-plugin-unused-imports": "^1.1.5", "fs-extra": "^11.1.1", @@ -33,7 +40,10 @@ "jest": "^29.5.0", "jest-environment-jsdom": "^29.5.0", "lint-staged": "^13.2.2", - "semver": "^7.5.1" + "react": "^18.2.0", + "react-dom": "^18.2.0", + "semver": "^7.5.1", + "vite": "^4.3.9" }, "engines": { "node": ">=18.14.0" @@ -53,42 +63,42 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.21.4.tgz", - "integrity": "sha512-LYvhNKfwWSPpocw8GI7gpK2nq3HSDuEPC/uSYaALSJu9xjsalaaYFOq0Pwt5KmVqwEbZlDu81aLXwBOmD/Fv9g==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.5.tgz", + "integrity": "sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==", "dev": true, "dependencies": { - "@babel/highlight": "^7.18.6" + "@babel/highlight": "^7.22.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/compat-data": { - "version": "7.21.5", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.21.5.tgz", - "integrity": "sha512-M+XAiQ7GzQ3FDPf0KOLkugzptnIypt0X0ma0wmlTKPR3IchgNFdx2JXxZdvd18JY5s7QkaFD/qyX0dsMpog/Ug==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.5.tgz", + "integrity": "sha512-4Jc/YuIaYqKnDDz892kPIledykKg12Aw1PYX5i/TY28anJtacvM1Rrr8wbieB9GfEJwlzqT0hUEao0CxEebiDA==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.21.5", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.21.5.tgz", - "integrity": "sha512-9M398B/QH5DlfCOTKDZT1ozXr0x8uBEeFd+dJraGUZGiaNpGCDVGCc14hZexsMblw3XxltJ+6kSvogp9J+5a9g==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.22.5.tgz", + "integrity": "sha512-SBuTAjg91A3eKOvD+bPEz3LlhHZRNu1nFOVts9lzDJTXshHTjII0BAtDS3Y2DAkdZdDKWVZGVwkDfc4Clxn1dg==", "dev": true, "dependencies": { "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.21.4", - "@babel/generator": "^7.21.5", - "@babel/helper-compilation-targets": "^7.21.5", - "@babel/helper-module-transforms": "^7.21.5", - "@babel/helpers": "^7.21.5", - "@babel/parser": "^7.21.5", - "@babel/template": "^7.20.7", - "@babel/traverse": "^7.21.5", - "@babel/types": "^7.21.5", + "@babel/code-frame": "^7.22.5", + "@babel/generator": "^7.22.5", + "@babel/helper-compilation-targets": "^7.22.5", + "@babel/helper-module-transforms": "^7.22.5", + "@babel/helpers": "^7.22.5", + "@babel/parser": "^7.22.5", + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.5", + "@babel/types": "^7.22.5", "convert-source-map": "^1.7.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -119,12 +129,12 @@ } }, "node_modules/@babel/generator": { - "version": "7.21.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.21.5.tgz", - "integrity": "sha512-SrKK/sRv8GesIW1bDagf9cCG38IOMYZusoe1dfg0D8aiUe3Amvoj1QtjTPAWcfrZFvIwlleLb0gxzQidL9w14w==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.5.tgz", + "integrity": "sha512-+lcUbnTRhd0jOewtFSedLyiPsD5tswKkbgcezOqqWFUVNEwoUTlpPOBmvhG7OXWLR4jMdv0czPGH5XbflnD1EA==", "dev": true, "dependencies": { - "@babel/types": "^7.21.5", + "@babel/types": "^7.22.5", "@jridgewell/gen-mapping": "^0.3.2", "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" @@ -173,13 +183,13 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.21.5", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.21.5.tgz", - "integrity": "sha512-1RkbFGUKex4lvsB9yhIfWltJM5cZKUftB2eNajaDv3dCMEp49iBG0K14uH8NnX9IPux2+mK7JGEOB0jn48/J6w==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.5.tgz", + "integrity": "sha512-Ji+ywpHeuqxB8WDxraCiqR0xfhYjiDE/e6k7FuIaANnoOFxAHskHChz4vA1mJC9Lbm01s1PVAGhQY4FUKSkGZw==", "dev": true, "dependencies": { - "@babel/compat-data": "^7.21.5", - "@babel/helper-validator-option": "^7.21.0", + "@babel/compat-data": "^7.22.5", + "@babel/helper-validator-option": "^7.22.5", "browserslist": "^4.21.3", "lru-cache": "^5.1.1", "semver": "^6.3.0" @@ -265,9 +275,9 @@ } }, "node_modules/@babel/helper-environment-visitor": { - "version": "7.21.5", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.21.5.tgz", - "integrity": "sha512-IYl4gZ3ETsWocUWgsFZLM5i1BYx9SoemminVEXadgLBa9TdeorzgLKm8wWLA6J1N/kT3Kch8XIk1laNzYoHKvQ==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz", + "integrity": "sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==", "dev": true, "engines": { "node": ">=6.9.0" @@ -286,25 +296,25 @@ } }, "node_modules/@babel/helper-function-name": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.21.0.tgz", - "integrity": "sha512-HfK1aMRanKHpxemaY2gqBmL04iAPOPRj7DxtNbiDOrJK+gdwkiNRVpCpUJYbUT+aZyemKN8brqTOxzCaG6ExRg==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz", + "integrity": "sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==", "dev": true, "dependencies": { - "@babel/template": "^7.20.7", - "@babel/types": "^7.21.0" + "@babel/template": "^7.22.5", + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-hoist-variables": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", - "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", "dev": true, "dependencies": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -323,31 +333,31 @@ } }, "node_modules/@babel/helper-module-imports": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.21.4.tgz", - "integrity": "sha512-orajc5T2PsRYUN3ZryCEFeMDYwyw09c/pZeaQEZPH0MpKzSvn3e0uXsDBu3k03VI+9DBiRo+l22BfKTpKwa/Wg==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.5.tgz", + "integrity": "sha512-8Dl6+HD/cKifutF5qGd/8ZJi84QeAKh+CEe1sBzz8UayBBGg1dAIJrdHOcOM5b2MpzWL2yuotJTtGjETq0qjXg==", "dev": true, "dependencies": { - "@babel/types": "^7.21.4" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.21.5", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.21.5.tgz", - "integrity": "sha512-bI2Z9zBGY2q5yMHoBvJ2a9iX3ZOAzJPm7Q8Yz6YeoUjU/Cvhmi2G4QyTNyPBqqXSgTjUxRg3L0xV45HvkNWWBw==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.22.5.tgz", + "integrity": "sha512-+hGKDt/Ze8GFExiVHno/2dvG5IdstpzCq0y4Qc9OJ25D4q3pKfiIP/4Vp3/JvhDkLKsDK2api3q3fpIgiIF5bw==", "dev": true, "dependencies": { - "@babel/helper-environment-visitor": "^7.21.5", - "@babel/helper-module-imports": "^7.21.4", - "@babel/helper-simple-access": "^7.21.5", - "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/helper-validator-identifier": "^7.19.1", - "@babel/template": "^7.20.7", - "@babel/traverse": "^7.21.5", - "@babel/types": "^7.21.5" + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-module-imports": "^7.22.5", + "@babel/helper-simple-access": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.5", + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.5", + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -366,9 +376,9 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.21.5", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.21.5.tgz", - "integrity": "sha512-0WDaIlXKOX/3KfBK/dwP1oQGiPh6rjMkT7HIRv7i5RR2VUMwrx5ZL0dwBkKx7+SW1zwNdgjHd34IMk5ZjTeHVg==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", + "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", "dev": true, "engines": { "node": ">=6.9.0" @@ -410,12 +420,12 @@ } }, "node_modules/@babel/helper-simple-access": { - "version": "7.21.5", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.21.5.tgz", - "integrity": "sha512-ENPDAMC1wAjR0uaCUwliBdiSl1KBJAVnMTzXqi64c2MG8MPR6ii4qf7bSXDqSFbr4W6W028/rf5ivoHop5/mkg==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", + "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", "dev": true, "dependencies": { - "@babel/types": "^7.21.5" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -434,39 +444,39 @@ } }, "node_modules/@babel/helper-split-export-declaration": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", - "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.5.tgz", + "integrity": "sha512-thqK5QFghPKWLhAV321lxF95yCg2K3Ob5yw+M3VHWfdia0IkPXUtoLH8x/6Fh486QUvzhb8YOWHChTVen2/PoQ==", "dev": true, "dependencies": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-string-parser": { - "version": "7.21.5", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.21.5.tgz", - "integrity": "sha512-5pTUx3hAJaZIdW99sJ6ZUUgWq/Y+Hja7TowEnLNMm1VivRgZQL3vpBY3qUACVsvw+yQU6+YgfBVmcbLaZtrA1w==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", + "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", - "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz", + "integrity": "sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-option": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.21.0.tgz", - "integrity": "sha512-rmL/B8/f0mKS2baE9ZpyTcTavvEuWhTTW8amjzXNvYG4AwBsqTLikfXsEofsJEfKHf+HQVQbFOHy6o+4cnC/fQ==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.5.tgz", + "integrity": "sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw==", "dev": true, "engines": { "node": ">=6.9.0" @@ -488,26 +498,26 @@ } }, "node_modules/@babel/helpers": { - "version": "7.21.5", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.21.5.tgz", - "integrity": "sha512-BSY+JSlHxOmGsPTydUkPf1MdMQ3M81x5xGCOVgWM3G8XH77sJ292Y2oqcp0CbbgxhqBuI46iUz1tT7hqP7EfgA==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.22.5.tgz", + "integrity": "sha512-pSXRmfE1vzcUIDFQcSGA5Mr+GxBV9oiRKDuDxXvWQQBCh8HoIjs/2DlDB7H8smac1IVrB9/xdXj2N3Wol9Cr+Q==", "dev": true, "dependencies": { - "@babel/template": "^7.20.7", - "@babel/traverse": "^7.21.5", - "@babel/types": "^7.21.5" + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.5", + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/highlight": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", - "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.5.tgz", + "integrity": "sha512-BSKlD1hgnedS5XRnGOljZawtag7H1yPfQp0tdNJCHoH6AZ+Pcm9VvkrK59/Yy593Ypg0zMxH2BxD1VPYUQ7UIw==", "dev": true, "dependencies": { - "@babel/helper-validator-identifier": "^7.18.6", + "@babel/helper-validator-identifier": "^7.22.5", "chalk": "^2.0.0", "js-tokens": "^4.0.0" }, @@ -587,9 +597,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.21.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.21.5.tgz", - "integrity": "sha512-J+IxH2IsxV4HbnTrSWgMAQj0UEo61hDA4Ny8h8PCX0MLXiibqHbqIOVneqdocemSBc22VpBKxt4J6FQzy9HarQ==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.5.tgz", + "integrity": "sha512-DFZMC9LJUG9PLOclRC32G63UXwzqS2koQC8dkx+PLdmt1xSePYpbT/NbsrJy8Q/muXz7o/h/d4A7Fuyixm559Q==", "dev": true, "bin": { "parser": "bin/babel-parser.js" @@ -1494,6 +1504,36 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.22.5.tgz", + "integrity": "sha512-nTh2ogNUtxbiSbxaT4Ds6aXnXEipHweN9YRgOX/oNXdf0cCrGn/+2LozFa3lnPV5D90MkjhgckCPBrsoSc1a7g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.22.5.tgz", + "integrity": "sha512-yIiRO6yobeEIaI0RTbIr8iAK9FcBHLtZq0S89ZPjDLQXBA4xvghaKqI0etp/tF3htTM0sazJKKLz9oEiGRtu7w==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-regenerator": { "version": "7.21.5", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.21.5.tgz", @@ -1765,33 +1805,33 @@ } }, "node_modules/@babel/template": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz", - "integrity": "sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.5.tgz", + "integrity": "sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.18.6", - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7" + "@babel/code-frame": "^7.22.5", + "@babel/parser": "^7.22.5", + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.21.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.21.5.tgz", - "integrity": "sha512-AhQoI3YjWi6u/y/ntv7k48mcrCXmus0t79J9qPNlk/lAsFlCiJ047RmbfMOawySTHtywXhbXgpx/8nXMYd+oFw==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.21.4", - "@babel/generator": "^7.21.5", - "@babel/helper-environment-visitor": "^7.21.5", - "@babel/helper-function-name": "^7.21.0", - "@babel/helper-hoist-variables": "^7.18.6", - "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/parser": "^7.21.5", - "@babel/types": "^7.21.5", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.5.tgz", + "integrity": "sha512-7DuIjPgERaNo6r+PZwItpjCZEa5vyw4eJGufeLxrPdBXBoLcCJCIasvK6pK/9DVNrLZTLFhUGqaC6X/PA007TQ==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.22.5", + "@babel/generator": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-function-name": "^7.22.5", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.5", + "@babel/parser": "^7.22.5", + "@babel/types": "^7.22.5", "debug": "^4.1.0", "globals": "^11.1.0" }, @@ -1800,13 +1840,13 @@ } }, "node_modules/@babel/types": { - "version": "7.21.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.21.5.tgz", - "integrity": "sha512-m4AfNvVF2mVC/F7fDEdH2El3HzUg9It/XsCxZiOTTA3m3qYfcSVSbTfM6Q9xG+hYDniZssYhlXKKUMD5m8tF4Q==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.5.tgz", + "integrity": "sha512-zo3MIHGOkPOfoRXitsgHLjEXmlDaD/5KU1Uzuc9GNiZPhSqVxVRtxuPaSBZDsYZ9qV88AjtMtWW7ww98loJ9KA==", "dev": true, "dependencies": { - "@babel/helper-string-parser": "^7.21.5", - "@babel/helper-validator-identifier": "^7.19.1", + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.5", "to-fast-properties": "^2.0.0" }, "engines": { @@ -2792,6 +2832,38 @@ "integrity": "sha512-KufADq8uQqo1pYKVIYzfKbJfBAc0sOeXqGbFaSpv8MRmC/zXgowNZmFcbngndGk922QDmOASEXUZCaY48gs4cg==", "dev": true }, + "node_modules/@types/prop-types": { + "version": "15.7.5", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", + "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==", + "dev": true + }, + "node_modules/@types/react": { + "version": "18.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.14.tgz", + "integrity": "sha512-A0zjq+QN/O0Kpe30hA1GidzyFjatVvrpIvWLxD+xv67Vt91TWWgco9IvrJBkeyHm1trGaFS/FSGqPlhyeZRm0g==", + "dev": true, + "dependencies": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.2.6", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.6.tgz", + "integrity": "sha512-2et4PDvg6PVCyS7fuTc4gPoksV58bW0RwSxWKcPRcHZf0PRUGq03TKcD/rUHe3azfV6/5/biUBJw+HhCQjaP0A==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/scheduler": { + "version": "0.16.3", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz", + "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==", + "dev": true + }, "node_modules/@types/semver": { "version": "7.5.0", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.0.tgz", @@ -2937,6 +3009,24 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@vitejs/plugin-react": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.0.1.tgz", + "integrity": "sha512-g25lL98essfeSj43HJ0o4DMp0325XK0ITkxpgChzJU/CyemgyChtlxfnRbjfwxDGCTRxTiXtQAsdebQXKMRSOA==", + "dev": true, + "dependencies": { + "@babel/core": "^7.22.5", + "@babel/plugin-transform-react-jsx-self": "^7.22.5", + "@babel/plugin-transform-react-jsx-source": "^7.22.5", + "react-refresh": "^0.14.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0" + } + }, "node_modules/abab": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", @@ -3171,6 +3261,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.1.tgz", + "integrity": "sha512-pZYPXPRl2PqWcsUs6LOMn+1f1532nEoPTYowBtqLwAW+W8vSVhkIGnmOX1t/UQjD6YGI0vcD2B1U7ZFGQH9jnQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", + "es-shim-unscopables": "^1.0.0", + "get-intrinsic": "^1.1.3" + } + }, "node_modules/astral-regex": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", @@ -3855,6 +3958,12 @@ "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", "dev": true }, + "node_modules/csstype": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", + "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==", + "dev": true + }, "node_modules/data-urls": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz", @@ -4447,6 +4556,94 @@ } } }, + "node_modules/eslint-plugin-react": { + "version": "7.32.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.32.2.tgz", + "integrity": "sha512-t2fBMa+XzonrrNkyVirzKlvn5RXzzPwRHtMvLAtVZrt8oxgnTQaYbU6SXTOO1mwQgp1y5+toMSKInnzGr0Knqg==", + "dev": true, + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flatmap": "^1.3.1", + "array.prototype.tosorted": "^1.1.1", + "doctrine": "^2.1.0", + "estraverse": "^5.3.0", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.6", + "object.fromentries": "^2.0.6", + "object.hasown": "^1.1.2", + "object.values": "^1.1.6", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.4", + "semver": "^6.3.0", + "string.prototype.matchall": "^4.0.8" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz", + "integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==", + "dev": true, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.3.5.tgz", + "integrity": "sha512-61qNIsc7fo9Pp/mju0J83kzvLm0Bsayu7OQSLEoJxLDCBjIIyb87bkzufoOvdDxLkSlMfkF7UxomC4+eztUBSA==", + "dev": true, + "peerDependencies": { + "eslint": ">=7" + } + }, + "node_modules/eslint-plugin-react/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-react/node_modules/resolve": { + "version": "2.0.0-next.4", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.4.tgz", + "integrity": "sha512-iMDbmAWtfU+MHpxt/I5iWI7cY6YVEZUQ3MBgPQ++XD1PELuJHIl82xBmObyP2KyQmkNB2dsqF7seoQQiAn5yDQ==", + "dev": true, + "dependencies": { + "is-core-module": "^2.9.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/eslint-plugin-react/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/eslint-plugin-sort-keys": { "version": "2.3.5", "resolved": "https://registry.npmjs.org/eslint-plugin-sort-keys/-/eslint-plugin-sort-keys-2.3.5.tgz", @@ -6461,6 +6658,11 @@ "node": ">=4" } }, + "node_modules/json-logic-js": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/json-logic-js/-/json-logic-js-2.0.2.tgz", + "integrity": "sha512-ZBtBdMJieqQcH7IX/LaBsr5pX+Y5JIW+EhejtM3Ffg2jdN9Iwf+Ht6TbHnvAZ/YtwyuhPaCBlnvzrwVeWdvGDQ==" + }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", @@ -6503,6 +6705,21 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jsx-ast-utils": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.4.tgz", + "integrity": "sha512-fX2TVdCViod6HwKEtSWGHs57oFhVfCMwieb9PuRDgjDPh5XeqJiHFFFJCHxU5cnTc3Bu/GRL+kPiFmw8XWOfKw==", + "dev": true, + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, "node_modules/kleur": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", @@ -6830,6 +7047,18 @@ "node": ">=8" } }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -6957,6 +7186,24 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, + "node_modules/nanoid": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -7011,6 +7258,15 @@ "integrity": "sha512-90yv+6538zuvUMnN+zCr8LuV6bPFdq50304114vJYJ8RDyK8D5O9Phpbd6SZWgI7PwzmmfN1upeOJlvybDSgCw==", "dev": true }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-inspect": { "version": "1.12.3", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", @@ -7047,6 +7303,50 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/object.entries": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.6.tgz", + "integrity": "sha512-leTPzo4Zvg3pmbQ3rDK69Rl8GQvIqMWubrkxONG9/ojtFE2rD9fjMKfSI5BxW3osRH1m6VdzmqK8oAY9aT4x5w==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.6.tgz", + "integrity": "sha512-VciD13dswC4j1Xt5394WR4MzmAQmlgN72phd/riNp9vtD7tp4QQWJ0R4wvclXcafgcYK8veHRed2W6XeGBvcfg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.hasown": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.2.tgz", + "integrity": "sha512-B5UIT3J1W+WuWIU55h0mjlwaqxiE5vYENJXIXZ4VFe05pNYrkKuK0U/6aFcb0pKywYJh7IhfoqUfKVmrJJHZHw==", + "dev": true, + "dependencies": { + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/object.values": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.6.tgz", @@ -7306,6 +7606,34 @@ "node": ">=8" } }, + "node_modules/postcss": { + "version": "8.4.24", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.24.tgz", + "integrity": "sha512-M0RzbcI0sO/XJNucsGjvWU9ERWxb/ytp1w6dKtxTKgixdtQDq4rmx/g8W1hnaheq9jgwL/oyEdH5Bc4WwJKMqg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, "node_modules/prelude-ls": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", @@ -7397,6 +7725,23 @@ "node": ">= 6" } }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true + }, "node_modules/property-expr": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.4.tgz", @@ -7477,14 +7822,48 @@ "node": ">=4" } }, - "node_modules/react-is": { + "node_modules/react": { "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", - "dev": true - }, - "node_modules/regenerate": { - "version": "1.4.2", + "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "dev": true, + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", + "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "dev": true, + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.0" + }, + "peerDependencies": { + "react": "^18.2.0" + } + }, + "node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + }, + "node_modules/react-refresh": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz", + "integrity": "sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/regenerate": { + "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", "dev": true @@ -7705,6 +8084,22 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/rollup": { + "version": "3.25.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.25.3.tgz", + "integrity": "sha512-ZT279hx8gszBj9uy5FfhoG4bZx8c+0A1sbqtr7Q3KNWIizpTdDEPZbV2xcbvHsnFp4MavCQYZyzApJ+virB8Yw==", + "dev": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=14.18.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -7769,6 +8164,15 @@ "node": ">=v12.22.7" } }, + "node_modules/scheduler": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", + "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", + "dev": true, + "dependencies": { + "loose-envify": "^1.1.0" + } + }, "node_modules/semver": { "version": "7.5.1", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.1.tgz", @@ -7884,6 +8288,15 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-support": { "version": "0.5.13", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", @@ -7948,6 +8361,25 @@ "node": ">=8" } }, + "node_modules/string.prototype.matchall": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.8.tgz", + "integrity": "sha512-6zOCOcJ+RJAQshcTvXPHoxoQGONa3e/Lqx90wUA+wEzX78sg5Bo+1tQo4N0pohS0erG9qtCqJDjNCQBjeWVxyg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", + "get-intrinsic": "^1.1.3", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.3", + "regexp.prototype.flags": "^1.4.3", + "side-channel": "^1.0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/string.prototype.trim": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.7.tgz", @@ -8453,6 +8885,54 @@ "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", "dev": true }, + "node_modules/vite": { + "version": "4.3.9", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.3.9.tgz", + "integrity": "sha512-qsTNZjO9NoJNW7KnOrgYwczm0WctJ8m/yqYAMAK9Lxt4SoySUfS5S8ia9K7JHpa3KEeMfyF8LoJ3c5NeBJy6pg==", + "dev": true, + "dependencies": { + "esbuild": "^0.17.5", + "postcss": "^8.4.23", + "rollup": "^3.21.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "@types/node": ">= 14", + "less": "*", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, "node_modules/w3c-xmlserializer": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", @@ -8740,36 +9220,36 @@ } }, "@babel/code-frame": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.21.4.tgz", - "integrity": "sha512-LYvhNKfwWSPpocw8GI7gpK2nq3HSDuEPC/uSYaALSJu9xjsalaaYFOq0Pwt5KmVqwEbZlDu81aLXwBOmD/Fv9g==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.5.tgz", + "integrity": "sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==", "dev": true, "requires": { - "@babel/highlight": "^7.18.6" + "@babel/highlight": "^7.22.5" } }, "@babel/compat-data": { - "version": "7.21.5", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.21.5.tgz", - "integrity": "sha512-M+XAiQ7GzQ3FDPf0KOLkugzptnIypt0X0ma0wmlTKPR3IchgNFdx2JXxZdvd18JY5s7QkaFD/qyX0dsMpog/Ug==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.5.tgz", + "integrity": "sha512-4Jc/YuIaYqKnDDz892kPIledykKg12Aw1PYX5i/TY28anJtacvM1Rrr8wbieB9GfEJwlzqT0hUEao0CxEebiDA==", "dev": true }, "@babel/core": { - "version": "7.21.5", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.21.5.tgz", - "integrity": "sha512-9M398B/QH5DlfCOTKDZT1ozXr0x8uBEeFd+dJraGUZGiaNpGCDVGCc14hZexsMblw3XxltJ+6kSvogp9J+5a9g==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.22.5.tgz", + "integrity": "sha512-SBuTAjg91A3eKOvD+bPEz3LlhHZRNu1nFOVts9lzDJTXshHTjII0BAtDS3Y2DAkdZdDKWVZGVwkDfc4Clxn1dg==", "dev": true, "requires": { "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.21.4", - "@babel/generator": "^7.21.5", - "@babel/helper-compilation-targets": "^7.21.5", - "@babel/helper-module-transforms": "^7.21.5", - "@babel/helpers": "^7.21.5", - "@babel/parser": "^7.21.5", - "@babel/template": "^7.20.7", - "@babel/traverse": "^7.21.5", - "@babel/types": "^7.21.5", + "@babel/code-frame": "^7.22.5", + "@babel/generator": "^7.22.5", + "@babel/helper-compilation-targets": "^7.22.5", + "@babel/helper-module-transforms": "^7.22.5", + "@babel/helpers": "^7.22.5", + "@babel/parser": "^7.22.5", + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.5", + "@babel/types": "^7.22.5", "convert-source-map": "^1.7.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -8792,12 +9272,12 @@ } }, "@babel/generator": { - "version": "7.21.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.21.5.tgz", - "integrity": "sha512-SrKK/sRv8GesIW1bDagf9cCG38IOMYZusoe1dfg0D8aiUe3Amvoj1QtjTPAWcfrZFvIwlleLb0gxzQidL9w14w==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.5.tgz", + "integrity": "sha512-+lcUbnTRhd0jOewtFSedLyiPsD5tswKkbgcezOqqWFUVNEwoUTlpPOBmvhG7OXWLR4jMdv0czPGH5XbflnD1EA==", "dev": true, "requires": { - "@babel/types": "^7.21.5", + "@babel/types": "^7.22.5", "@jridgewell/gen-mapping": "^0.3.2", "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" @@ -8836,13 +9316,13 @@ } }, "@babel/helper-compilation-targets": { - "version": "7.21.5", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.21.5.tgz", - "integrity": "sha512-1RkbFGUKex4lvsB9yhIfWltJM5cZKUftB2eNajaDv3dCMEp49iBG0K14uH8NnX9IPux2+mK7JGEOB0jn48/J6w==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.5.tgz", + "integrity": "sha512-Ji+ywpHeuqxB8WDxraCiqR0xfhYjiDE/e6k7FuIaANnoOFxAHskHChz4vA1mJC9Lbm01s1PVAGhQY4FUKSkGZw==", "dev": true, "requires": { - "@babel/compat-data": "^7.21.5", - "@babel/helper-validator-option": "^7.21.0", + "@babel/compat-data": "^7.22.5", + "@babel/helper-validator-option": "^7.22.5", "browserslist": "^4.21.3", "lru-cache": "^5.1.1", "semver": "^6.3.0" @@ -8905,9 +9385,9 @@ } }, "@babel/helper-environment-visitor": { - "version": "7.21.5", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.21.5.tgz", - "integrity": "sha512-IYl4gZ3ETsWocUWgsFZLM5i1BYx9SoemminVEXadgLBa9TdeorzgLKm8wWLA6J1N/kT3Kch8XIk1laNzYoHKvQ==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz", + "integrity": "sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==", "dev": true }, "@babel/helper-explode-assignable-expression": { @@ -8920,22 +9400,22 @@ } }, "@babel/helper-function-name": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.21.0.tgz", - "integrity": "sha512-HfK1aMRanKHpxemaY2gqBmL04iAPOPRj7DxtNbiDOrJK+gdwkiNRVpCpUJYbUT+aZyemKN8brqTOxzCaG6ExRg==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz", + "integrity": "sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==", "dev": true, "requires": { - "@babel/template": "^7.20.7", - "@babel/types": "^7.21.0" + "@babel/template": "^7.22.5", + "@babel/types": "^7.22.5" } }, "@babel/helper-hoist-variables": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", - "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", "dev": true, "requires": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" } }, "@babel/helper-member-expression-to-functions": { @@ -8948,28 +9428,28 @@ } }, "@babel/helper-module-imports": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.21.4.tgz", - "integrity": "sha512-orajc5T2PsRYUN3ZryCEFeMDYwyw09c/pZeaQEZPH0MpKzSvn3e0uXsDBu3k03VI+9DBiRo+l22BfKTpKwa/Wg==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.5.tgz", + "integrity": "sha512-8Dl6+HD/cKifutF5qGd/8ZJi84QeAKh+CEe1sBzz8UayBBGg1dAIJrdHOcOM5b2MpzWL2yuotJTtGjETq0qjXg==", "dev": true, "requires": { - "@babel/types": "^7.21.4" + "@babel/types": "^7.22.5" } }, "@babel/helper-module-transforms": { - "version": "7.21.5", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.21.5.tgz", - "integrity": "sha512-bI2Z9zBGY2q5yMHoBvJ2a9iX3ZOAzJPm7Q8Yz6YeoUjU/Cvhmi2G4QyTNyPBqqXSgTjUxRg3L0xV45HvkNWWBw==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.22.5.tgz", + "integrity": "sha512-+hGKDt/Ze8GFExiVHno/2dvG5IdstpzCq0y4Qc9OJ25D4q3pKfiIP/4Vp3/JvhDkLKsDK2api3q3fpIgiIF5bw==", "dev": true, "requires": { - "@babel/helper-environment-visitor": "^7.21.5", - "@babel/helper-module-imports": "^7.21.4", - "@babel/helper-simple-access": "^7.21.5", - "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/helper-validator-identifier": "^7.19.1", - "@babel/template": "^7.20.7", - "@babel/traverse": "^7.21.5", - "@babel/types": "^7.21.5" + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-module-imports": "^7.22.5", + "@babel/helper-simple-access": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.5", + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.5", + "@babel/types": "^7.22.5" } }, "@babel/helper-optimise-call-expression": { @@ -8982,9 +9462,9 @@ } }, "@babel/helper-plugin-utils": { - "version": "7.21.5", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.21.5.tgz", - "integrity": "sha512-0WDaIlXKOX/3KfBK/dwP1oQGiPh6rjMkT7HIRv7i5RR2VUMwrx5ZL0dwBkKx7+SW1zwNdgjHd34IMk5ZjTeHVg==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", + "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", "dev": true }, "@babel/helper-remap-async-to-generator": { @@ -9014,12 +9494,12 @@ } }, "@babel/helper-simple-access": { - "version": "7.21.5", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.21.5.tgz", - "integrity": "sha512-ENPDAMC1wAjR0uaCUwliBdiSl1KBJAVnMTzXqi64c2MG8MPR6ii4qf7bSXDqSFbr4W6W028/rf5ivoHop5/mkg==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", + "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", "dev": true, "requires": { - "@babel/types": "^7.21.5" + "@babel/types": "^7.22.5" } }, "@babel/helper-skip-transparent-expression-wrappers": { @@ -9032,30 +9512,30 @@ } }, "@babel/helper-split-export-declaration": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", - "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.5.tgz", + "integrity": "sha512-thqK5QFghPKWLhAV321lxF95yCg2K3Ob5yw+M3VHWfdia0IkPXUtoLH8x/6Fh486QUvzhb8YOWHChTVen2/PoQ==", "dev": true, "requires": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" } }, "@babel/helper-string-parser": { - "version": "7.21.5", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.21.5.tgz", - "integrity": "sha512-5pTUx3hAJaZIdW99sJ6ZUUgWq/Y+Hja7TowEnLNMm1VivRgZQL3vpBY3qUACVsvw+yQU6+YgfBVmcbLaZtrA1w==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", + "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", "dev": true }, "@babel/helper-validator-identifier": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", - "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz", + "integrity": "sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==", "dev": true }, "@babel/helper-validator-option": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.21.0.tgz", - "integrity": "sha512-rmL/B8/f0mKS2baE9ZpyTcTavvEuWhTTW8amjzXNvYG4AwBsqTLikfXsEofsJEfKHf+HQVQbFOHy6o+4cnC/fQ==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.5.tgz", + "integrity": "sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw==", "dev": true }, "@babel/helper-wrap-function": { @@ -9071,23 +9551,23 @@ } }, "@babel/helpers": { - "version": "7.21.5", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.21.5.tgz", - "integrity": "sha512-BSY+JSlHxOmGsPTydUkPf1MdMQ3M81x5xGCOVgWM3G8XH77sJ292Y2oqcp0CbbgxhqBuI46iUz1tT7hqP7EfgA==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.22.5.tgz", + "integrity": "sha512-pSXRmfE1vzcUIDFQcSGA5Mr+GxBV9oiRKDuDxXvWQQBCh8HoIjs/2DlDB7H8smac1IVrB9/xdXj2N3Wol9Cr+Q==", "dev": true, "requires": { - "@babel/template": "^7.20.7", - "@babel/traverse": "^7.21.5", - "@babel/types": "^7.21.5" + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.5", + "@babel/types": "^7.22.5" } }, "@babel/highlight": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", - "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.5.tgz", + "integrity": "sha512-BSKlD1hgnedS5XRnGOljZawtag7H1yPfQp0tdNJCHoH6AZ+Pcm9VvkrK59/Yy593Ypg0zMxH2BxD1VPYUQ7UIw==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.18.6", + "@babel/helper-validator-identifier": "^7.22.5", "chalk": "^2.0.0", "js-tokens": "^4.0.0" }, @@ -9151,9 +9631,9 @@ } }, "@babel/parser": { - "version": "7.21.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.21.5.tgz", - "integrity": "sha512-J+IxH2IsxV4HbnTrSWgMAQj0UEo61hDA4Ny8h8PCX0MLXiibqHbqIOVneqdocemSBc22VpBKxt4J6FQzy9HarQ==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.5.tgz", + "integrity": "sha512-DFZMC9LJUG9PLOclRC32G63UXwzqS2koQC8dkx+PLdmt1xSePYpbT/NbsrJy8Q/muXz7o/h/d4A7Fuyixm559Q==", "dev": true }, "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { @@ -9737,6 +10217,24 @@ "@babel/helper-plugin-utils": "^7.18.6" } }, + "@babel/plugin-transform-react-jsx-self": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.22.5.tgz", + "integrity": "sha512-nTh2ogNUtxbiSbxaT4Ds6aXnXEipHweN9YRgOX/oNXdf0cCrGn/+2LozFa3lnPV5D90MkjhgckCPBrsoSc1a7g==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-react-jsx-source": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.22.5.tgz", + "integrity": "sha512-yIiRO6yobeEIaI0RTbIr8iAK9FcBHLtZq0S89ZPjDLQXBA4xvghaKqI0etp/tF3htTM0sazJKKLz9oEiGRtu7w==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, "@babel/plugin-transform-regenerator": { "version": "7.21.5", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.21.5.tgz", @@ -9941,42 +10439,42 @@ } }, "@babel/template": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz", - "integrity": "sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.5.tgz", + "integrity": "sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==", "dev": true, "requires": { - "@babel/code-frame": "^7.18.6", - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7" + "@babel/code-frame": "^7.22.5", + "@babel/parser": "^7.22.5", + "@babel/types": "^7.22.5" } }, "@babel/traverse": { - "version": "7.21.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.21.5.tgz", - "integrity": "sha512-AhQoI3YjWi6u/y/ntv7k48mcrCXmus0t79J9qPNlk/lAsFlCiJ047RmbfMOawySTHtywXhbXgpx/8nXMYd+oFw==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.21.4", - "@babel/generator": "^7.21.5", - "@babel/helper-environment-visitor": "^7.21.5", - "@babel/helper-function-name": "^7.21.0", - "@babel/helper-hoist-variables": "^7.18.6", - "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/parser": "^7.21.5", - "@babel/types": "^7.21.5", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.5.tgz", + "integrity": "sha512-7DuIjPgERaNo6r+PZwItpjCZEa5vyw4eJGufeLxrPdBXBoLcCJCIasvK6pK/9DVNrLZTLFhUGqaC6X/PA007TQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.22.5", + "@babel/generator": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-function-name": "^7.22.5", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.5", + "@babel/parser": "^7.22.5", + "@babel/types": "^7.22.5", "debug": "^4.1.0", "globals": "^11.1.0" } }, "@babel/types": { - "version": "7.21.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.21.5.tgz", - "integrity": "sha512-m4AfNvVF2mVC/F7fDEdH2El3HzUg9It/XsCxZiOTTA3m3qYfcSVSbTfM6Q9xG+hYDniZssYhlXKKUMD5m8tF4Q==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.5.tgz", + "integrity": "sha512-zo3MIHGOkPOfoRXitsgHLjEXmlDaD/5KU1Uzuc9GNiZPhSqVxVRtxuPaSBZDsYZ9qV88AjtMtWW7ww98loJ9KA==", "dev": true, "requires": { - "@babel/helper-string-parser": "^7.21.5", - "@babel/helper-validator-identifier": "^7.19.1", + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.5", "to-fast-properties": "^2.0.0" } }, @@ -10650,6 +11148,38 @@ "integrity": "sha512-KufADq8uQqo1pYKVIYzfKbJfBAc0sOeXqGbFaSpv8MRmC/zXgowNZmFcbngndGk922QDmOASEXUZCaY48gs4cg==", "dev": true }, + "@types/prop-types": { + "version": "15.7.5", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", + "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==", + "dev": true + }, + "@types/react": { + "version": "18.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.14.tgz", + "integrity": "sha512-A0zjq+QN/O0Kpe30hA1GidzyFjatVvrpIvWLxD+xv67Vt91TWWgco9IvrJBkeyHm1trGaFS/FSGqPlhyeZRm0g==", + "dev": true, + "requires": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "@types/react-dom": { + "version": "18.2.6", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.6.tgz", + "integrity": "sha512-2et4PDvg6PVCyS7fuTc4gPoksV58bW0RwSxWKcPRcHZf0PRUGq03TKcD/rUHe3azfV6/5/biUBJw+HhCQjaP0A==", + "dev": true, + "requires": { + "@types/react": "*" + } + }, + "@types/scheduler": { + "version": "0.16.3", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz", + "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==", + "dev": true + }, "@types/semver": { "version": "7.5.0", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.0.tgz", @@ -10748,6 +11278,18 @@ } } }, + "@vitejs/plugin-react": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.0.1.tgz", + "integrity": "sha512-g25lL98essfeSj43HJ0o4DMp0325XK0ITkxpgChzJU/CyemgyChtlxfnRbjfwxDGCTRxTiXtQAsdebQXKMRSOA==", + "dev": true, + "requires": { + "@babel/core": "^7.22.5", + "@babel/plugin-transform-react-jsx-self": "^7.22.5", + "@babel/plugin-transform-react-jsx-source": "^7.22.5", + "react-refresh": "^0.14.0" + } + }, "abab": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", @@ -10916,6 +11458,19 @@ "es-shim-unscopables": "^1.0.0" } }, + "array.prototype.tosorted": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.1.tgz", + "integrity": "sha512-pZYPXPRl2PqWcsUs6LOMn+1f1532nEoPTYowBtqLwAW+W8vSVhkIGnmOX1t/UQjD6YGI0vcD2B1U7ZFGQH9jnQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", + "es-shim-unscopables": "^1.0.0", + "get-intrinsic": "^1.1.3" + } + }, "astral-regex": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", @@ -11421,6 +11976,12 @@ } } }, + "csstype": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", + "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==", + "dev": true + }, "data-urls": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz", @@ -11936,6 +12497,71 @@ "prettier-linter-helpers": "^1.0.0" } }, + "eslint-plugin-react": { + "version": "7.32.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.32.2.tgz", + "integrity": "sha512-t2fBMa+XzonrrNkyVirzKlvn5RXzzPwRHtMvLAtVZrt8oxgnTQaYbU6SXTOO1mwQgp1y5+toMSKInnzGr0Knqg==", + "dev": true, + "requires": { + "array-includes": "^3.1.6", + "array.prototype.flatmap": "^1.3.1", + "array.prototype.tosorted": "^1.1.1", + "doctrine": "^2.1.0", + "estraverse": "^5.3.0", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.6", + "object.fromentries": "^2.0.6", + "object.hasown": "^1.1.2", + "object.values": "^1.1.6", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.4", + "semver": "^6.3.0", + "string.prototype.matchall": "^4.0.8" + }, + "dependencies": { + "doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "resolve": { + "version": "2.0.0-next.4", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.4.tgz", + "integrity": "sha512-iMDbmAWtfU+MHpxt/I5iWI7cY6YVEZUQ3MBgPQ++XD1PELuJHIl82xBmObyP2KyQmkNB2dsqF7seoQQiAn5yDQ==", + "dev": true, + "requires": { + "is-core-module": "^2.9.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "eslint-plugin-react-hooks": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz", + "integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==", + "dev": true, + "requires": {} + }, + "eslint-plugin-react-refresh": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.3.5.tgz", + "integrity": "sha512-61qNIsc7fo9Pp/mju0J83kzvLm0Bsayu7OQSLEoJxLDCBjIIyb87bkzufoOvdDxLkSlMfkF7UxomC4+eztUBSA==", + "dev": true, + "requires": {} + }, "eslint-plugin-sort-keys": { "version": "2.3.5", "resolved": "https://registry.npmjs.org/eslint-plugin-sort-keys/-/eslint-plugin-sort-keys-2.3.5.tgz", @@ -13342,6 +13968,11 @@ "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", "dev": true }, + "json-logic-js": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/json-logic-js/-/json-logic-js-2.0.2.tgz", + "integrity": "sha512-ZBtBdMJieqQcH7IX/LaBsr5pX+Y5JIW+EhejtM3Ffg2jdN9Iwf+Ht6TbHnvAZ/YtwyuhPaCBlnvzrwVeWdvGDQ==" + }, "json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", @@ -13376,6 +14007,18 @@ "universalify": "^2.0.0" } }, + "jsx-ast-utils": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.4.tgz", + "integrity": "sha512-fX2TVdCViod6HwKEtSWGHs57oFhVfCMwieb9PuRDgjDPh5XeqJiHFFFJCHxU5cnTc3Bu/GRL+kPiFmw8XWOfKw==", + "dev": true, + "requires": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + } + }, "kleur": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", @@ -13605,6 +14248,15 @@ } } }, + "loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" + } + }, "lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -13704,6 +14356,12 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, + "nanoid": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "dev": true + }, "natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -13749,6 +14407,12 @@ "integrity": "sha512-90yv+6538zuvUMnN+zCr8LuV6bPFdq50304114vJYJ8RDyK8D5O9Phpbd6SZWgI7PwzmmfN1upeOJlvybDSgCw==", "dev": true }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true + }, "object-inspect": { "version": "1.12.3", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", @@ -13773,6 +14437,38 @@ "object-keys": "^1.1.1" } }, + "object.entries": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.6.tgz", + "integrity": "sha512-leTPzo4Zvg3pmbQ3rDK69Rl8GQvIqMWubrkxONG9/ojtFE2rD9fjMKfSI5BxW3osRH1m6VdzmqK8oAY9aT4x5w==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + } + }, + "object.fromentries": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.6.tgz", + "integrity": "sha512-VciD13dswC4j1Xt5394WR4MzmAQmlgN72phd/riNp9vtD7tp4QQWJ0R4wvclXcafgcYK8veHRed2W6XeGBvcfg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + } + }, + "object.hasown": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.2.tgz", + "integrity": "sha512-B5UIT3J1W+WuWIU55h0mjlwaqxiE5vYENJXIXZ4VFe05pNYrkKuK0U/6aFcb0pKywYJh7IhfoqUfKVmrJJHZHw==", + "dev": true, + "requires": { + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + } + }, "object.values": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.6.tgz", @@ -13953,6 +14649,17 @@ "find-up": "^4.0.0" } }, + "postcss": { + "version": "8.4.24", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.24.tgz", + "integrity": "sha512-M0RzbcI0sO/XJNucsGjvWU9ERWxb/ytp1w6dKtxTKgixdtQDq4rmx/g8W1hnaheq9jgwL/oyEdH5Bc4WwJKMqg==", + "dev": true, + "requires": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + } + }, "prelude-ls": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", @@ -14016,6 +14723,25 @@ "sisteransi": "^1.0.5" } }, + "prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, + "requires": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + }, + "dependencies": { + "react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true + } + } + }, "property-expr": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.4.tgz", @@ -14066,12 +14792,37 @@ "ret": "^0.2.0" } }, + "react": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "dev": true, + "requires": { + "loose-envify": "^1.1.0" + } + }, + "react-dom": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", + "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "dev": true, + "requires": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.0" + } + }, "react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", "dev": true }, + "react-refresh": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz", + "integrity": "sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==", + "dev": true + }, "regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", @@ -14235,6 +14986,15 @@ "glob": "^7.1.3" } }, + "rollup": { + "version": "3.25.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.25.3.tgz", + "integrity": "sha512-ZT279hx8gszBj9uy5FfhoG4bZx8c+0A1sbqtr7Q3KNWIizpTdDEPZbV2xcbvHsnFp4MavCQYZyzApJ+virB8Yw==", + "dev": true, + "requires": { + "fsevents": "~2.3.2" + } + }, "run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -14279,6 +15039,15 @@ "xmlchars": "^2.2.0" } }, + "scheduler": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", + "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", + "dev": true, + "requires": { + "loose-envify": "^1.1.0" + } + }, "semver": { "version": "7.5.1", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.1.tgz", @@ -14366,6 +15135,12 @@ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true }, + "source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true + }, "source-map-support": { "version": "0.5.13", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", @@ -14418,6 +15193,22 @@ "strip-ansi": "^6.0.1" } }, + "string.prototype.matchall": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.8.tgz", + "integrity": "sha512-6zOCOcJ+RJAQshcTvXPHoxoQGONa3e/Lqx90wUA+wEzX78sg5Bo+1tQo4N0pohS0erG9qtCqJDjNCQBjeWVxyg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", + "get-intrinsic": "^1.1.3", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.3", + "regexp.prototype.flags": "^1.4.3", + "side-channel": "^1.0.4" + } + }, "string.prototype.trim": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.7.tgz", @@ -14801,6 +15592,18 @@ } } }, + "vite": { + "version": "4.3.9", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.3.9.tgz", + "integrity": "sha512-qsTNZjO9NoJNW7KnOrgYwczm0WctJ8m/yqYAMAK9Lxt4SoySUfS5S8ia9K7JHpa3KEeMfyF8LoJ3c5NeBJy6pg==", + "dev": true, + "requires": { + "esbuild": "^0.17.5", + "fsevents": "~2.3.2", + "postcss": "^8.4.23", + "rollup": "^3.21.0" + } + }, "w3c-xmlserializer": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", diff --git a/package.json b/package.json index ddb40bc33..b6e329fb5 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ ], "scripts": { "build": "node scripts/build.js", + "dev": "vite serve playground", "test": "jest", "test:watch": "jest --watchAll", "lint": "eslint \"src/**/*.{js,ts}\"", @@ -47,6 +48,7 @@ ] }, "dependencies": { + "json-logic-js": "^2.0.2", "lodash": "^4.17.21", "randexp": "^0.5.3", "yup": "^0.30.0" @@ -71,7 +73,16 @@ "jest": "^29.5.0", "jest-environment-jsdom": "^29.5.0", "lint-staged": "^13.2.2", - "semver": "^7.5.1" + "semver": "^7.5.1", + "@types/react": "^18.0.37", + "@types/react-dom": "^18.0.11", + "@vitejs/plugin-react": "^4.0.0", + "eslint-plugin-react": "^7.32.2", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.3.4", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "vite": "^4.3.9" }, "keywords": [ "json schemas", diff --git a/playground/index.html b/playground/index.html new file mode 100644 index 000000000..79c470191 --- /dev/null +++ b/playground/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + React + + +
+ + + diff --git a/playground/src/App.jsx b/playground/src/App.jsx new file mode 100644 index 000000000..1a2a5165d --- /dev/null +++ b/playground/src/App.jsx @@ -0,0 +1,7 @@ +import { createHeadlessForm } from '../../src' + +function App() { + return <>{String(createHeadlessForm)}; +} + +export default App; diff --git a/playground/src/main.jsx b/playground/src/main.jsx new file mode 100644 index 000000000..51a8c5825 --- /dev/null +++ b/playground/src/main.jsx @@ -0,0 +1,9 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import App from './App.jsx' + +ReactDOM.createRoot(document.getElementById('root')).render( + + + , +) diff --git a/playground/vite.config.js b/playground/vite.config.js new file mode 100644 index 000000000..5a33944a9 --- /dev/null +++ b/playground/vite.config.js @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react()], +}) From afade7df8731e3ebad945e629af5af89282d5bdc Mon Sep 17 00:00:00 2001 From: brennj Date: Fri, 30 Jun 2023 15:12:09 +0200 Subject: [PATCH 02/69] feat: poc of json-logic --- src/helpers.js | 13 +++++++++++-- src/jsonLogic.js | 20 ++++++++++++++++++++ src/yupSchema.js | 10 ++++++++++ 3 files changed, 41 insertions(+), 2 deletions(-) create mode 100644 src/jsonLogic.js diff --git a/src/helpers.js b/src/helpers.js index 5353488cd..d314a72b6 100644 --- a/src/helpers.js +++ b/src/helpers.js @@ -7,6 +7,7 @@ import { lazy } from 'yup'; import { supportedTypes, getInputType } from './internals/fields'; import { pickXKey } from './internals/helpers'; +import { processValidationRule } from './jsonLogic'; import { containsHTML, hasProperty, wrapWithSpan } from './utils'; import { buildCompleteYupSchema, buildYupSchema } from './yupSchema'; @@ -29,7 +30,7 @@ function hasType(type, typeName) { * @param {Object[]} fields - form fields * @returns */ -function getField(fieldName, fields) { +export function getField(fieldName, fields) { return fields.find(({ name }) => name === fieldName); } @@ -228,7 +229,7 @@ export function getPrefillValues(fields, initialValues = {}) { * @param {Object} node - JSON-schema node * @returns */ -function updateField(field, requiredFields, node, formValues) { +export function updateField(field, requiredFields, node, formValues) { // If there was an error building the field, it might not exist in the form even though // it can be mentioned in the schema so we return early in that case if (!field) { @@ -275,6 +276,10 @@ function updateField(field, requiredFields, node, formValues) { } }); + if (node?.target && field.name === node?.target) { + field.rules = Array.isArray(field.rules) ? field.rules.push(node) : [node]; + } + // If field has a calculateConditionalProperties closure, run it and update the field properties if (field.calculateConditionalProperties) { const newFieldValues = field.calculateConditionalProperties(fieldIsRequired, node); @@ -321,6 +326,10 @@ function processNode(node, formValues, formFields, accRequired = new Set()) { updateField(getField(fieldName, formFields), requiredFields, node, formValues); }); + node['x-jsf-validations']?.forEach((validation) => + processValidationRule(validation, formFields, requiredFields, node, formValues) + ); + if (node.if) { const matchesCondition = checkIfConditionMatches(node, formValues, formFields); // BUG HERE (unreleated) - what if it matches but doesn't has a then, diff --git a/src/jsonLogic.js b/src/jsonLogic.js new file mode 100644 index 000000000..ac38cee9c --- /dev/null +++ b/src/jsonLogic.js @@ -0,0 +1,20 @@ +import jsonLogic from 'json-logic-js'; + +import { getField, updateField } from './helpers'; + +export function processValidationRule(validation, formFields, requiredFields, node, formValues) { + const { target } = validation; + const field = getField(target, formFields); + updateField(field, requiredFields, validation, formValues); +} + +export function yupSchemaWithCustomJSONLogic(validation) { + return (yupSchema) => + yupSchema.test( + 'randomName', + validation.errorMessage ?? 'This field is invalid.', + (_, { parent }) => { + return jsonLogic.apply(validation.rule, parent); + } + ); +} diff --git a/src/yupSchema.js b/src/yupSchema.js index c8ab1ba01..be36197f0 100644 --- a/src/yupSchema.js +++ b/src/yupSchema.js @@ -4,6 +4,7 @@ import { randexp } from 'randexp'; import { string, number, boolean, object, array } from 'yup'; import { supportedTypes } from './internals/fields'; +import { yupSchemaWithCustomJSONLogic } from './jsonLogic'; import { convertDiskSizeFromTo } from './utils'; /** @@ -334,6 +335,11 @@ export function buildYupSchema(field, config) { if (propertyFields.accept) { validators.push(withFileFormat); } + + if (propertyFields.rules) { + propertyFields.rules.forEach((rule) => validators.push(yupSchemaWithCustomJSONLogic(rule))); + } + return flow(validators); } @@ -368,6 +374,10 @@ function getSchema(fields = [], config) { Object.assign(newSchema, getSchema(field.fields, config)); } } + // For custom json-logic rules, rebuild the schema. + if (field.rules) { + newSchema[field.name] = buildYupSchema(field, config)(); + } }); return newSchema; From 23f654502e54649ed8d4b67b411bf884a898931a Mon Sep 17 00:00:00 2001 From: brennj Date: Mon, 3 Jul 2023 18:37:06 +0200 Subject: [PATCH 03/69] chore: more stable implementation --- src/helpers.js | 11 ++--------- src/jsonLogic.js | 8 -------- src/yupSchema.js | 6 ++++-- 3 files changed, 6 insertions(+), 19 deletions(-) diff --git a/src/helpers.js b/src/helpers.js index d314a72b6..f3bf7549d 100644 --- a/src/helpers.js +++ b/src/helpers.js @@ -7,7 +7,6 @@ import { lazy } from 'yup'; import { supportedTypes, getInputType } from './internals/fields'; import { pickXKey } from './internals/helpers'; -import { processValidationRule } from './jsonLogic'; import { containsHTML, hasProperty, wrapWithSpan } from './utils'; import { buildCompleteYupSchema, buildYupSchema } from './yupSchema'; @@ -276,10 +275,6 @@ export function updateField(field, requiredFields, node, formValues) { } }); - if (node?.target && field.name === node?.target) { - field.rules = Array.isArray(field.rules) ? field.rules.push(node) : [node]; - } - // If field has a calculateConditionalProperties closure, run it and update the field properties if (field.calculateConditionalProperties) { const newFieldValues = field.calculateConditionalProperties(fieldIsRequired, node); @@ -326,10 +321,6 @@ function processNode(node, formValues, formFields, accRequired = new Set()) { updateField(getField(fieldName, formFields), requiredFields, node, formValues); }); - node['x-jsf-validations']?.forEach((validation) => - processValidationRule(validation, formFields, requiredFields, node, formValues) - ); - if (node.if) { const matchesCondition = checkIfConditionMatches(node, formValues, formFields); // BUG HERE (unreleated) - what if it matches but doesn't has a then, @@ -483,6 +474,7 @@ export function extractParametersFromNode(schemaNode) { const presentation = pickXKey(schemaNode, 'presentation') ?? {}; const errorMessage = pickXKey(schemaNode, 'errorMessage') ?? {}; + const validations = schemaNode['x-jsf-validations']; const node = omit(schemaNode, ['x-jsf-presentation', 'presentation']); @@ -527,6 +519,7 @@ export function extractParametersFromNode(schemaNode) { // Handle [name].presentation ...presentation, + validations: validations, description: containsHTML(description) ? wrapWithSpan(description, { class: 'jsf-description', diff --git a/src/jsonLogic.js b/src/jsonLogic.js index ac38cee9c..39d0b34eb 100644 --- a/src/jsonLogic.js +++ b/src/jsonLogic.js @@ -1,13 +1,5 @@ import jsonLogic from 'json-logic-js'; -import { getField, updateField } from './helpers'; - -export function processValidationRule(validation, formFields, requiredFields, node, formValues) { - const { target } = validation; - const field = getField(target, formFields); - updateField(field, requiredFields, validation, formValues); -} - export function yupSchemaWithCustomJSONLogic(validation) { return (yupSchema) => yupSchema.test( diff --git a/src/yupSchema.js b/src/yupSchema.js index be36197f0..fa84fa8be 100644 --- a/src/yupSchema.js +++ b/src/yupSchema.js @@ -336,8 +336,10 @@ export function buildYupSchema(field, config) { validators.push(withFileFormat); } - if (propertyFields.rules) { - propertyFields.rules.forEach((rule) => validators.push(yupSchemaWithCustomJSONLogic(rule))); + if (propertyFields.validations) { + propertyFields.validations.forEach((rule) => + validators.push(yupSchemaWithCustomJSONLogic(rule)) + ); } return flow(validators); From c3811861fc397d8abc5bcf1ec603da3fa9fcf42d Mon Sep 17 00:00:00 2001 From: brennj Date: Tue, 4 Jul 2023 12:41:01 +0200 Subject: [PATCH 04/69] chore: remove playground for now --- playground/index.html | 13 ------------- playground/src/App.jsx | 7 ------- playground/src/main.jsx | 9 --------- playground/vite.config.js | 7 ------- 4 files changed, 36 deletions(-) delete mode 100644 playground/index.html delete mode 100644 playground/src/App.jsx delete mode 100644 playground/src/main.jsx delete mode 100644 playground/vite.config.js diff --git a/playground/index.html b/playground/index.html deleted file mode 100644 index 79c470191..000000000 --- a/playground/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - Vite + React - - -
- - - diff --git a/playground/src/App.jsx b/playground/src/App.jsx deleted file mode 100644 index 1a2a5165d..000000000 --- a/playground/src/App.jsx +++ /dev/null @@ -1,7 +0,0 @@ -import { createHeadlessForm } from '../../src' - -function App() { - return <>{String(createHeadlessForm)}; -} - -export default App; diff --git a/playground/src/main.jsx b/playground/src/main.jsx deleted file mode 100644 index 51a8c5825..000000000 --- a/playground/src/main.jsx +++ /dev/null @@ -1,9 +0,0 @@ -import React from 'react' -import ReactDOM from 'react-dom/client' -import App from './App.jsx' - -ReactDOM.createRoot(document.getElementById('root')).render( - - - , -) diff --git a/playground/vite.config.js b/playground/vite.config.js deleted file mode 100644 index 5a33944a9..000000000 --- a/playground/vite.config.js +++ /dev/null @@ -1,7 +0,0 @@ -import { defineConfig } from 'vite' -import react from '@vitejs/plugin-react' - -// https://vitejs.dev/config/ -export default defineConfig({ - plugins: [react()], -}) From e6eef4ea535a40e2b268a8fa313a4584f2cd0c15 Mon Sep 17 00:00:00 2001 From: brennj Date: Tue, 4 Jul 2023 12:42:42 +0200 Subject: [PATCH 05/69] chore: remove unneeded stuff --- package-lock.json | 795 +--------------------------------------------- package.json | 12 +- src/helpers.js | 4 +- 3 files changed, 4 insertions(+), 807 deletions(-) diff --git a/package-lock.json b/package-lock.json index e4254ec70..99427ee33 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,9 +17,6 @@ "devDependencies": { "@babel/core": "^7.21.5", "@babel/preset-env": "^7.21.5", - "@types/react": "^18.0.37", - "@types/react-dom": "^18.0.11", - "@vitejs/plugin-react": "^4.0.0", "babel-eslint": "^10.1.0", "babel-jest": "^29.5.0", "child-process-promise": "^2.2.1", @@ -29,9 +26,6 @@ "eslint-plugin-import": "^2.27.5", "eslint-plugin-jest": "^27.2.1", "eslint-plugin-prettier": "^4.2.1", - "eslint-plugin-react": "^7.32.2", - "eslint-plugin-react-hooks": "^4.6.0", - "eslint-plugin-react-refresh": "^0.3.4", "eslint-plugin-sort-keys": "^2.3.5", "eslint-plugin-unused-imports": "^1.1.5", "fs-extra": "^11.1.1", @@ -39,11 +33,7 @@ "husky": "^8.0.3", "jest": "^29.5.0", "jest-environment-jsdom": "^29.5.0", - "lint-staged": "^13.2.2", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "semver": "^7.5.1", - "vite": "^4.3.9" + "lint-staged": "^13.2.2" }, "engines": { "node": ">=18.14.0" @@ -1504,36 +1494,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-react-jsx-self": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.22.5.tgz", - "integrity": "sha512-nTh2ogNUtxbiSbxaT4Ds6aXnXEipHweN9YRgOX/oNXdf0cCrGn/+2LozFa3lnPV5D90MkjhgckCPBrsoSc1a7g==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-source": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.22.5.tgz", - "integrity": "sha512-yIiRO6yobeEIaI0RTbIr8iAK9FcBHLtZq0S89ZPjDLQXBA4xvghaKqI0etp/tF3htTM0sazJKKLz9oEiGRtu7w==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/plugin-transform-regenerator": { "version": "7.21.5", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.21.5.tgz", @@ -2832,38 +2792,6 @@ "integrity": "sha512-KufADq8uQqo1pYKVIYzfKbJfBAc0sOeXqGbFaSpv8MRmC/zXgowNZmFcbngndGk922QDmOASEXUZCaY48gs4cg==", "dev": true }, - "node_modules/@types/prop-types": { - "version": "15.7.5", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", - "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==", - "dev": true - }, - "node_modules/@types/react": { - "version": "18.2.14", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.14.tgz", - "integrity": "sha512-A0zjq+QN/O0Kpe30hA1GidzyFjatVvrpIvWLxD+xv67Vt91TWWgco9IvrJBkeyHm1trGaFS/FSGqPlhyeZRm0g==", - "dev": true, - "dependencies": { - "@types/prop-types": "*", - "@types/scheduler": "*", - "csstype": "^3.0.2" - } - }, - "node_modules/@types/react-dom": { - "version": "18.2.6", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.6.tgz", - "integrity": "sha512-2et4PDvg6PVCyS7fuTc4gPoksV58bW0RwSxWKcPRcHZf0PRUGq03TKcD/rUHe3azfV6/5/biUBJw+HhCQjaP0A==", - "dev": true, - "dependencies": { - "@types/react": "*" - } - }, - "node_modules/@types/scheduler": { - "version": "0.16.3", - "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz", - "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==", - "dev": true - }, "node_modules/@types/semver": { "version": "7.5.0", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.0.tgz", @@ -3009,24 +2937,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/@vitejs/plugin-react": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.0.1.tgz", - "integrity": "sha512-g25lL98essfeSj43HJ0o4DMp0325XK0ITkxpgChzJU/CyemgyChtlxfnRbjfwxDGCTRxTiXtQAsdebQXKMRSOA==", - "dev": true, - "dependencies": { - "@babel/core": "^7.22.5", - "@babel/plugin-transform-react-jsx-self": "^7.22.5", - "@babel/plugin-transform-react-jsx-source": "^7.22.5", - "react-refresh": "^0.14.0" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "peerDependencies": { - "vite": "^4.2.0" - } - }, "node_modules/abab": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", @@ -3261,19 +3171,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/array.prototype.tosorted": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.1.tgz", - "integrity": "sha512-pZYPXPRl2PqWcsUs6LOMn+1f1532nEoPTYowBtqLwAW+W8vSVhkIGnmOX1t/UQjD6YGI0vcD2B1U7ZFGQH9jnQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", - "es-shim-unscopables": "^1.0.0", - "get-intrinsic": "^1.1.3" - } - }, "node_modules/astral-regex": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", @@ -3958,12 +3855,6 @@ "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", "dev": true }, - "node_modules/csstype": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", - "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==", - "dev": true - }, "node_modules/data-urls": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz", @@ -4556,94 +4447,6 @@ } } }, - "node_modules/eslint-plugin-react": { - "version": "7.32.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.32.2.tgz", - "integrity": "sha512-t2fBMa+XzonrrNkyVirzKlvn5RXzzPwRHtMvLAtVZrt8oxgnTQaYbU6SXTOO1mwQgp1y5+toMSKInnzGr0Knqg==", - "dev": true, - "dependencies": { - "array-includes": "^3.1.6", - "array.prototype.flatmap": "^1.3.1", - "array.prototype.tosorted": "^1.1.1", - "doctrine": "^2.1.0", - "estraverse": "^5.3.0", - "jsx-ast-utils": "^2.4.1 || ^3.0.0", - "minimatch": "^3.1.2", - "object.entries": "^1.1.6", - "object.fromentries": "^2.0.6", - "object.hasown": "^1.1.2", - "object.values": "^1.1.6", - "prop-types": "^15.8.1", - "resolve": "^2.0.0-next.4", - "semver": "^6.3.0", - "string.prototype.matchall": "^4.0.8" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" - } - }, - "node_modules/eslint-plugin-react-hooks": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz", - "integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==", - "dev": true, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" - } - }, - "node_modules/eslint-plugin-react-refresh": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.3.5.tgz", - "integrity": "sha512-61qNIsc7fo9Pp/mju0J83kzvLm0Bsayu7OQSLEoJxLDCBjIIyb87bkzufoOvdDxLkSlMfkF7UxomC4+eztUBSA==", - "dev": true, - "peerDependencies": { - "eslint": ">=7" - } - }, - "node_modules/eslint-plugin-react/node_modules/doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dev": true, - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/eslint-plugin-react/node_modules/resolve": { - "version": "2.0.0-next.4", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.4.tgz", - "integrity": "sha512-iMDbmAWtfU+MHpxt/I5iWI7cY6YVEZUQ3MBgPQ++XD1PELuJHIl82xBmObyP2KyQmkNB2dsqF7seoQQiAn5yDQ==", - "dev": true, - "dependencies": { - "is-core-module": "^2.9.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/eslint-plugin-react/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/eslint-plugin-sort-keys": { "version": "2.3.5", "resolved": "https://registry.npmjs.org/eslint-plugin-sort-keys/-/eslint-plugin-sort-keys-2.3.5.tgz", @@ -6705,21 +6508,6 @@ "graceful-fs": "^4.1.6" } }, - "node_modules/jsx-ast-utils": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.4.tgz", - "integrity": "sha512-fX2TVdCViod6HwKEtSWGHs57oFhVfCMwieb9PuRDgjDPh5XeqJiHFFFJCHxU5cnTc3Bu/GRL+kPiFmw8XWOfKw==", - "dev": true, - "dependencies": { - "array-includes": "^3.1.6", - "array.prototype.flat": "^1.3.1", - "object.assign": "^4.1.4", - "object.values": "^1.1.6" - }, - "engines": { - "node": ">=4.0" - } - }, "node_modules/kleur": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", @@ -7047,18 +6835,6 @@ "node": ">=8" } }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -7186,24 +6962,6 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, - "node_modules/nanoid": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", - "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -7258,15 +7016,6 @@ "integrity": "sha512-90yv+6538zuvUMnN+zCr8LuV6bPFdq50304114vJYJ8RDyK8D5O9Phpbd6SZWgI7PwzmmfN1upeOJlvybDSgCw==", "dev": true }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/object-inspect": { "version": "1.12.3", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", @@ -7303,50 +7052,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/object.entries": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.6.tgz", - "integrity": "sha512-leTPzo4Zvg3pmbQ3rDK69Rl8GQvIqMWubrkxONG9/ojtFE2rD9fjMKfSI5BxW3osRH1m6VdzmqK8oAY9aT4x5w==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.fromentries": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.6.tgz", - "integrity": "sha512-VciD13dswC4j1Xt5394WR4MzmAQmlgN72phd/riNp9vtD7tp4QQWJ0R4wvclXcafgcYK8veHRed2W6XeGBvcfg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.hasown": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.2.tgz", - "integrity": "sha512-B5UIT3J1W+WuWIU55h0mjlwaqxiE5vYENJXIXZ4VFe05pNYrkKuK0U/6aFcb0pKywYJh7IhfoqUfKVmrJJHZHw==", - "dev": true, - "dependencies": { - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/object.values": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.6.tgz", @@ -7606,34 +7311,6 @@ "node": ">=8" } }, - "node_modules/postcss": { - "version": "8.4.24", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.24.tgz", - "integrity": "sha512-M0RzbcI0sO/XJNucsGjvWU9ERWxb/ytp1w6dKtxTKgixdtQDq4rmx/g8W1hnaheq9jgwL/oyEdH5Bc4WwJKMqg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "nanoid": "^3.3.6", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, "node_modules/prelude-ls": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", @@ -7725,23 +7402,6 @@ "node": ">= 6" } }, - "node_modules/prop-types": { - "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dev": true, - "dependencies": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" - } - }, - "node_modules/prop-types/node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true - }, "node_modules/property-expr": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.4.tgz", @@ -7822,46 +7482,12 @@ "node": ">=4" } }, - "node_modules/react": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", - "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", - "dev": true, - "dependencies": { - "loose-envify": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", - "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", - "dev": true, - "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.0" - }, - "peerDependencies": { - "react": "^18.2.0" - } - }, "node_modules/react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", "dev": true }, - "node_modules/react-refresh": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz", - "integrity": "sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", @@ -8084,22 +7710,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/rollup": { - "version": "3.25.3", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.25.3.tgz", - "integrity": "sha512-ZT279hx8gszBj9uy5FfhoG4bZx8c+0A1sbqtr7Q3KNWIizpTdDEPZbV2xcbvHsnFp4MavCQYZyzApJ+virB8Yw==", - "dev": true, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=14.18.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -8164,15 +7774,6 @@ "node": ">=v12.22.7" } }, - "node_modules/scheduler": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", - "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", - "dev": true, - "dependencies": { - "loose-envify": "^1.1.0" - } - }, "node_modules/semver": { "version": "7.5.1", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.1.tgz", @@ -8288,15 +7889,6 @@ "node": ">=0.10.0" } }, - "node_modules/source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/source-map-support": { "version": "0.5.13", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", @@ -8361,25 +7953,6 @@ "node": ">=8" } }, - "node_modules/string.prototype.matchall": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.8.tgz", - "integrity": "sha512-6zOCOcJ+RJAQshcTvXPHoxoQGONa3e/Lqx90wUA+wEzX78sg5Bo+1tQo4N0pohS0erG9qtCqJDjNCQBjeWVxyg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", - "get-intrinsic": "^1.1.3", - "has-symbols": "^1.0.3", - "internal-slot": "^1.0.3", - "regexp.prototype.flags": "^1.4.3", - "side-channel": "^1.0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/string.prototype.trim": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.7.tgz", @@ -8885,54 +8458,6 @@ "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", "dev": true }, - "node_modules/vite": { - "version": "4.3.9", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.3.9.tgz", - "integrity": "sha512-qsTNZjO9NoJNW7KnOrgYwczm0WctJ8m/yqYAMAK9Lxt4SoySUfS5S8ia9K7JHpa3KEeMfyF8LoJ3c5NeBJy6pg==", - "dev": true, - "dependencies": { - "esbuild": "^0.17.5", - "postcss": "^8.4.23", - "rollup": "^3.21.0" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - }, - "peerDependencies": { - "@types/node": ">= 14", - "less": "*", - "sass": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "less": { - "optional": true - }, - "sass": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - } - } - }, "node_modules/w3c-xmlserializer": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", @@ -10217,24 +9742,6 @@ "@babel/helper-plugin-utils": "^7.18.6" } }, - "@babel/plugin-transform-react-jsx-self": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.22.5.tgz", - "integrity": "sha512-nTh2ogNUtxbiSbxaT4Ds6aXnXEipHweN9YRgOX/oNXdf0cCrGn/+2LozFa3lnPV5D90MkjhgckCPBrsoSc1a7g==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-react-jsx-source": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.22.5.tgz", - "integrity": "sha512-yIiRO6yobeEIaI0RTbIr8iAK9FcBHLtZq0S89ZPjDLQXBA4xvghaKqI0etp/tF3htTM0sazJKKLz9oEiGRtu7w==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.22.5" - } - }, "@babel/plugin-transform-regenerator": { "version": "7.21.5", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.21.5.tgz", @@ -11148,38 +10655,6 @@ "integrity": "sha512-KufADq8uQqo1pYKVIYzfKbJfBAc0sOeXqGbFaSpv8MRmC/zXgowNZmFcbngndGk922QDmOASEXUZCaY48gs4cg==", "dev": true }, - "@types/prop-types": { - "version": "15.7.5", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", - "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==", - "dev": true - }, - "@types/react": { - "version": "18.2.14", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.14.tgz", - "integrity": "sha512-A0zjq+QN/O0Kpe30hA1GidzyFjatVvrpIvWLxD+xv67Vt91TWWgco9IvrJBkeyHm1trGaFS/FSGqPlhyeZRm0g==", - "dev": true, - "requires": { - "@types/prop-types": "*", - "@types/scheduler": "*", - "csstype": "^3.0.2" - } - }, - "@types/react-dom": { - "version": "18.2.6", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.6.tgz", - "integrity": "sha512-2et4PDvg6PVCyS7fuTc4gPoksV58bW0RwSxWKcPRcHZf0PRUGq03TKcD/rUHe3azfV6/5/biUBJw+HhCQjaP0A==", - "dev": true, - "requires": { - "@types/react": "*" - } - }, - "@types/scheduler": { - "version": "0.16.3", - "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz", - "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==", - "dev": true - }, "@types/semver": { "version": "7.5.0", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.0.tgz", @@ -11278,18 +10753,6 @@ } } }, - "@vitejs/plugin-react": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.0.1.tgz", - "integrity": "sha512-g25lL98essfeSj43HJ0o4DMp0325XK0ITkxpgChzJU/CyemgyChtlxfnRbjfwxDGCTRxTiXtQAsdebQXKMRSOA==", - "dev": true, - "requires": { - "@babel/core": "^7.22.5", - "@babel/plugin-transform-react-jsx-self": "^7.22.5", - "@babel/plugin-transform-react-jsx-source": "^7.22.5", - "react-refresh": "^0.14.0" - } - }, "abab": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", @@ -11458,19 +10921,6 @@ "es-shim-unscopables": "^1.0.0" } }, - "array.prototype.tosorted": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.1.tgz", - "integrity": "sha512-pZYPXPRl2PqWcsUs6LOMn+1f1532nEoPTYowBtqLwAW+W8vSVhkIGnmOX1t/UQjD6YGI0vcD2B1U7ZFGQH9jnQ==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", - "es-shim-unscopables": "^1.0.0", - "get-intrinsic": "^1.1.3" - } - }, "astral-regex": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", @@ -11976,12 +11426,6 @@ } } }, - "csstype": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", - "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==", - "dev": true - }, "data-urls": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz", @@ -12497,71 +11941,6 @@ "prettier-linter-helpers": "^1.0.0" } }, - "eslint-plugin-react": { - "version": "7.32.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.32.2.tgz", - "integrity": "sha512-t2fBMa+XzonrrNkyVirzKlvn5RXzzPwRHtMvLAtVZrt8oxgnTQaYbU6SXTOO1mwQgp1y5+toMSKInnzGr0Knqg==", - "dev": true, - "requires": { - "array-includes": "^3.1.6", - "array.prototype.flatmap": "^1.3.1", - "array.prototype.tosorted": "^1.1.1", - "doctrine": "^2.1.0", - "estraverse": "^5.3.0", - "jsx-ast-utils": "^2.4.1 || ^3.0.0", - "minimatch": "^3.1.2", - "object.entries": "^1.1.6", - "object.fromentries": "^2.0.6", - "object.hasown": "^1.1.2", - "object.values": "^1.1.6", - "prop-types": "^15.8.1", - "resolve": "^2.0.0-next.4", - "semver": "^6.3.0", - "string.prototype.matchall": "^4.0.8" - }, - "dependencies": { - "doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dev": true, - "requires": { - "esutils": "^2.0.2" - } - }, - "resolve": { - "version": "2.0.0-next.4", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.4.tgz", - "integrity": "sha512-iMDbmAWtfU+MHpxt/I5iWI7cY6YVEZUQ3MBgPQ++XD1PELuJHIl82xBmObyP2KyQmkNB2dsqF7seoQQiAn5yDQ==", - "dev": true, - "requires": { - "is-core-module": "^2.9.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - } - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, - "eslint-plugin-react-hooks": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz", - "integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==", - "dev": true, - "requires": {} - }, - "eslint-plugin-react-refresh": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.3.5.tgz", - "integrity": "sha512-61qNIsc7fo9Pp/mju0J83kzvLm0Bsayu7OQSLEoJxLDCBjIIyb87bkzufoOvdDxLkSlMfkF7UxomC4+eztUBSA==", - "dev": true, - "requires": {} - }, "eslint-plugin-sort-keys": { "version": "2.3.5", "resolved": "https://registry.npmjs.org/eslint-plugin-sort-keys/-/eslint-plugin-sort-keys-2.3.5.tgz", @@ -14007,18 +13386,6 @@ "universalify": "^2.0.0" } }, - "jsx-ast-utils": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.4.tgz", - "integrity": "sha512-fX2TVdCViod6HwKEtSWGHs57oFhVfCMwieb9PuRDgjDPh5XeqJiHFFFJCHxU5cnTc3Bu/GRL+kPiFmw8XWOfKw==", - "dev": true, - "requires": { - "array-includes": "^3.1.6", - "array.prototype.flat": "^1.3.1", - "object.assign": "^4.1.4", - "object.values": "^1.1.6" - } - }, "kleur": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", @@ -14248,15 +13615,6 @@ } } }, - "loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, - "requires": { - "js-tokens": "^3.0.0 || ^4.0.0" - } - }, "lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -14356,12 +13714,6 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, - "nanoid": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", - "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", - "dev": true - }, "natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -14407,12 +13759,6 @@ "integrity": "sha512-90yv+6538zuvUMnN+zCr8LuV6bPFdq50304114vJYJ8RDyK8D5O9Phpbd6SZWgI7PwzmmfN1upeOJlvybDSgCw==", "dev": true }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true - }, "object-inspect": { "version": "1.12.3", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", @@ -14437,38 +13783,6 @@ "object-keys": "^1.1.1" } }, - "object.entries": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.6.tgz", - "integrity": "sha512-leTPzo4Zvg3pmbQ3rDK69Rl8GQvIqMWubrkxONG9/ojtFE2rD9fjMKfSI5BxW3osRH1m6VdzmqK8oAY9aT4x5w==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" - } - }, - "object.fromentries": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.6.tgz", - "integrity": "sha512-VciD13dswC4j1Xt5394WR4MzmAQmlgN72phd/riNp9vtD7tp4QQWJ0R4wvclXcafgcYK8veHRed2W6XeGBvcfg==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" - } - }, - "object.hasown": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.2.tgz", - "integrity": "sha512-B5UIT3J1W+WuWIU55h0mjlwaqxiE5vYENJXIXZ4VFe05pNYrkKuK0U/6aFcb0pKywYJh7IhfoqUfKVmrJJHZHw==", - "dev": true, - "requires": { - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" - } - }, "object.values": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.6.tgz", @@ -14649,17 +13963,6 @@ "find-up": "^4.0.0" } }, - "postcss": { - "version": "8.4.24", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.24.tgz", - "integrity": "sha512-M0RzbcI0sO/XJNucsGjvWU9ERWxb/ytp1w6dKtxTKgixdtQDq4rmx/g8W1hnaheq9jgwL/oyEdH5Bc4WwJKMqg==", - "dev": true, - "requires": { - "nanoid": "^3.3.6", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - } - }, "prelude-ls": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", @@ -14723,25 +14026,6 @@ "sisteransi": "^1.0.5" } }, - "prop-types": { - "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dev": true, - "requires": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" - }, - "dependencies": { - "react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true - } - } - }, "property-expr": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.4.tgz", @@ -14792,37 +14076,12 @@ "ret": "^0.2.0" } }, - "react": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", - "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", - "dev": true, - "requires": { - "loose-envify": "^1.1.0" - } - }, - "react-dom": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", - "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", - "dev": true, - "requires": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.0" - } - }, "react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", "dev": true }, - "react-refresh": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz", - "integrity": "sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==", - "dev": true - }, "regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", @@ -14986,15 +14245,6 @@ "glob": "^7.1.3" } }, - "rollup": { - "version": "3.25.3", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.25.3.tgz", - "integrity": "sha512-ZT279hx8gszBj9uy5FfhoG4bZx8c+0A1sbqtr7Q3KNWIizpTdDEPZbV2xcbvHsnFp4MavCQYZyzApJ+virB8Yw==", - "dev": true, - "requires": { - "fsevents": "~2.3.2" - } - }, "run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -15039,15 +14289,6 @@ "xmlchars": "^2.2.0" } }, - "scheduler": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", - "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", - "dev": true, - "requires": { - "loose-envify": "^1.1.0" - } - }, "semver": { "version": "7.5.1", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.1.tgz", @@ -15135,12 +14376,6 @@ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true }, - "source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", - "dev": true - }, "source-map-support": { "version": "0.5.13", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", @@ -15193,22 +14428,6 @@ "strip-ansi": "^6.0.1" } }, - "string.prototype.matchall": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.8.tgz", - "integrity": "sha512-6zOCOcJ+RJAQshcTvXPHoxoQGONa3e/Lqx90wUA+wEzX78sg5Bo+1tQo4N0pohS0erG9qtCqJDjNCQBjeWVxyg==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", - "get-intrinsic": "^1.1.3", - "has-symbols": "^1.0.3", - "internal-slot": "^1.0.3", - "regexp.prototype.flags": "^1.4.3", - "side-channel": "^1.0.4" - } - }, "string.prototype.trim": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.7.tgz", @@ -15592,18 +14811,6 @@ } } }, - "vite": { - "version": "4.3.9", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.3.9.tgz", - "integrity": "sha512-qsTNZjO9NoJNW7KnOrgYwczm0WctJ8m/yqYAMAK9Lxt4SoySUfS5S8ia9K7JHpa3KEeMfyF8LoJ3c5NeBJy6pg==", - "dev": true, - "requires": { - "esbuild": "^0.17.5", - "fsevents": "~2.3.2", - "postcss": "^8.4.23", - "rollup": "^3.21.0" - } - }, "w3c-xmlserializer": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", diff --git a/package.json b/package.json index b6e329fb5..ff88130b0 100644 --- a/package.json +++ b/package.json @@ -72,17 +72,7 @@ "husky": "^8.0.3", "jest": "^29.5.0", "jest-environment-jsdom": "^29.5.0", - "lint-staged": "^13.2.2", - "semver": "^7.5.1", - "@types/react": "^18.0.37", - "@types/react-dom": "^18.0.11", - "@vitejs/plugin-react": "^4.0.0", - "eslint-plugin-react": "^7.32.2", - "eslint-plugin-react-hooks": "^4.6.0", - "eslint-plugin-react-refresh": "^0.3.4", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "vite": "^4.3.9" + "lint-staged": "^13.2.2" }, "keywords": [ "json schemas", diff --git a/src/helpers.js b/src/helpers.js index f3bf7549d..94d6bc28f 100644 --- a/src/helpers.js +++ b/src/helpers.js @@ -29,7 +29,7 @@ function hasType(type, typeName) { * @param {Object[]} fields - form fields * @returns */ -export function getField(fieldName, fields) { +function getField(fieldName, fields) { return fields.find(({ name }) => name === fieldName); } @@ -228,7 +228,7 @@ export function getPrefillValues(fields, initialValues = {}) { * @param {Object} node - JSON-schema node * @returns */ -export function updateField(field, requiredFields, node, formValues) { +function updateField(field, requiredFields, node, formValues) { // If there was an error building the field, it might not exist in the form even though // it can be mentioned in the schema so we return early in that case if (!field) { From 9e2f73cf1b88be15757fba5d6fc2062e9a59d5c8 Mon Sep 17 00:00:00 2001 From: brennj Date: Tue, 4 Jul 2023 12:43:08 +0200 Subject: [PATCH 06/69] chore: more unneeded --- .eslintrc | 1 - 1 file changed, 1 deletion(-) diff --git a/.eslintrc b/.eslintrc index 92d0fcf2a..91703edfe 100644 --- a/.eslintrc +++ b/.eslintrc @@ -2,7 +2,6 @@ "root": true, "extends": ["eslint:recommended", "prettier"], "plugins": ["import", "jest", "sort-keys", "unused-imports"], - "ignorePatterns": ["playground/**"], "parser": "babel-eslint", "env": { "es6": true, From e9656afeb074a8d87caf7be8381706785b43ee8f Mon Sep 17 00:00:00 2001 From: brennj Date: Tue, 4 Jul 2023 12:43:36 +0200 Subject: [PATCH 07/69] chore: clean up package.json --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index ff88130b0..cb4947fb7 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,6 @@ ], "scripts": { "build": "node scripts/build.js", - "dev": "vite serve playground", "test": "jest", "test:watch": "jest --watchAll", "lint": "eslint \"src/**/*.{js,ts}\"", From 4766be641f7c4aa76da4f3a3ac27b92c7301c457 Mon Sep 17 00:00:00 2001 From: brennj Date: Tue, 4 Jul 2023 12:44:08 +0200 Subject: [PATCH 08/69] chore: removed package by mistake --- package-lock.json | 3 ++- package.json | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 99427ee33..bb2adf88a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,7 +33,8 @@ "husky": "^8.0.3", "jest": "^29.5.0", "jest-environment-jsdom": "^29.5.0", - "lint-staged": "^13.2.2" + "lint-staged": "^13.2.2", + "semver": "^7.5.1" }, "engines": { "node": ">=18.14.0" diff --git a/package.json b/package.json index cb4947fb7..765de812c 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,8 @@ "husky": "^8.0.3", "jest": "^29.5.0", "jest-environment-jsdom": "^29.5.0", - "lint-staged": "^13.2.2" + "lint-staged": "^13.2.2", + "semver": "^7.5.1" }, "keywords": [ "json schemas", From f66c30fcca65f31a385fa7462509ea93af2c3480 Mon Sep 17 00:00:00 2001 From: brennj Date: Tue, 4 Jul 2023 12:52:51 +0200 Subject: [PATCH 09/69] chore: this isnt being used --- src/yupSchema.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/yupSchema.js b/src/yupSchema.js index fa84fa8be..796c8d6d2 100644 --- a/src/yupSchema.js +++ b/src/yupSchema.js @@ -376,10 +376,6 @@ function getSchema(fields = [], config) { Object.assign(newSchema, getSchema(field.fields, config)); } } - // For custom json-logic rules, rebuild the schema. - if (field.rules) { - newSchema[field.name] = buildYupSchema(field, config)(); - } }); return newSchema; From 25291aa107c18f29b9de2d3031d7b6fa2cf4b18d Mon Sep 17 00:00:00 2001 From: brennj Date: Tue, 4 Jul 2023 13:47:01 +0200 Subject: [PATCH 10/69] chore: first test passing --- src/tests/jsonLogic.test.js | 47 +++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 src/tests/jsonLogic.test.js diff --git a/src/tests/jsonLogic.test.js b/src/tests/jsonLogic.test.js new file mode 100644 index 000000000..49a07ad03 --- /dev/null +++ b/src/tests/jsonLogic.test.js @@ -0,0 +1,47 @@ +import { createHeadlessForm } from '../createHeadlessForm'; + +const schema = { + properties: { + field_a: { + type: 'number', + 'x-jsf-validations': [ + { + errorMessage: 'Field A must be bigger than field B', + rule: { '>': [{ var: 'field_a' }, { var: 'field_b' }] }, + }, + ], + }, + field_b: { + type: 'number', + }, + }, + required: ['field_a', 'field_b'], +}; + +describe('cross-value validations', () => { + describe('Relative: <, >, =', () => { + it('bigger: field_a > field_b', () => { + const { handleValidation } = createHeadlessForm(schema, { strictInputType: false }); + const { formErrors } = handleValidation({ field_a: 1, field_b: 2 }); + expect(formErrors.field_a).toEqual('Field A must be bigger than field B'); + }); + + it.todo('smaller: field_a < field_b'); + it.todo('equal: field_a = field_b'); + }); + + describe('Arithmetic: +, -, *, /', () => { + it.todo('multiple: field_a > field_b * 2'); // eg bonus is at least the double of salary + it.todo('divide: field_a > field_b / 2'); + it.todo('sum: field_a > field_b + field_c'); // eg salary is bigger than X and Y together. + }); + + describe('Logical: ||, &&', () => { + it.todo('AND: field_a > (field_b AND field_c)'); + it.todo('OR: field_a > (field_b OR field_c)'); + }); + + describe('Conditionals', () => { + it.todo('when field_a > field_b, show field_c'); + }); +}); From 1614f3e2153d54b70095313aabfe98de1dba0f82 Mon Sep 17 00:00:00 2001 From: brennj Date: Tue, 4 Jul 2023 13:51:32 +0200 Subject: [PATCH 11/69] chore: filling out the validations --- src/tests/jsonLogic.test.js | 67 ++++++++++++++++++++++++++----------- 1 file changed, 47 insertions(+), 20 deletions(-) diff --git a/src/tests/jsonLogic.test.js b/src/tests/jsonLogic.test.js index 49a07ad03..42c0e1474 100644 --- a/src/tests/jsonLogic.test.js +++ b/src/tests/jsonLogic.test.js @@ -1,37 +1,64 @@ import { createHeadlessForm } from '../createHeadlessForm'; -const schema = { - properties: { - field_a: { - type: 'number', - 'x-jsf-validations': [ - { - errorMessage: 'Field A must be bigger than field B', - rule: { '>': [{ var: 'field_a' }, { var: 'field_b' }] }, - }, - ], +function createSchemaWithRuleOnFieldA(rule) { + return { + properties: { + field_a: { + type: 'number', + 'x-jsf-validations': [rule], + }, + field_b: { + type: 'number', + }, }, - field_b: { - type: 'number', - }, - }, - required: ['field_a', 'field_b'], -}; + required: ['field_a', 'field_b'], + }; +} describe('cross-value validations', () => { describe('Relative: <, >, =', () => { it('bigger: field_a > field_b', () => { - const { handleValidation } = createHeadlessForm(schema, { strictInputType: false }); + const { handleValidation } = createHeadlessForm( + createSchemaWithRuleOnFieldA({ + errorMessage: 'Field A must be bigger than field B', + rule: { '>': [{ var: 'field_a' }, { var: 'field_b' }] }, + }), + { strictInputType: false } + ); const { formErrors } = handleValidation({ field_a: 1, field_b: 2 }); expect(formErrors.field_a).toEqual('Field A must be bigger than field B'); + expect(handleValidation({ field_a: 2, field_b: 0 }).formErrors).toEqual(undefined); + }); + + it('smaller: field_a < field_b', () => { + const { handleValidation } = createHeadlessForm( + createSchemaWithRuleOnFieldA({ + errorMessage: 'Field A must be smaller than field B', + rule: { '<': [{ var: 'field_a' }, { var: 'field_b' }] }, + }), + { strictInputType: false } + ); + const { formErrors } = handleValidation({ field_a: 2, field_b: 2 }); + expect(formErrors.field_a).toEqual('Field A must be smaller than field B'); + expect(handleValidation({ field_a: 0, field_b: 2 }).formErrors).toEqual(undefined); }); - it.todo('smaller: field_a < field_b'); - it.todo('equal: field_a = field_b'); + it('equal: field_a = field_b', () => { + const { handleValidation } = createHeadlessForm( + createSchemaWithRuleOnFieldA({ + errorMessage: 'Field A must equal field B', + rule: { '==': [{ var: 'field_a' }, { var: 'field_b' }] }, + }), + { strictInputType: false } + ); + const { formErrors } = handleValidation({ field_a: 3, field_b: 2 }); + expect(formErrors.field_a).toEqual('Field A must equal field B'); + expect(handleValidation({ field_a: 2, field_b: 2 }).formErrors).toEqual(undefined); + }); }); describe('Arithmetic: +, -, *, /', () => { - it.todo('multiple: field_a > field_b * 2'); // eg bonus is at least the double of salary + it.todo('multiple: field_a > field_b * 2'); it.todo('divide: field_a > field_b / 2'); it.todo('sum: field_a > field_b + field_c'); // eg salary is bigger than X and Y together. }); From 9ccd4c09cbbd547e1e243e45b01eda47fedb29b7 Mon Sep 17 00:00:00 2001 From: brennj Date: Tue, 4 Jul 2023 13:54:46 +0200 Subject: [PATCH 12/69] chore: more test cases todo --- src/tests/jsonLogic.test.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/tests/jsonLogic.test.js b/src/tests/jsonLogic.test.js index 42c0e1474..690976533 100644 --- a/src/tests/jsonLogic.test.js +++ b/src/tests/jsonLogic.test.js @@ -71,4 +71,13 @@ describe('cross-value validations', () => { describe('Conditionals', () => { it.todo('when field_a > field_b, show field_c'); }); + + describe('Multiple validations', () => { + it.todo('2 rules where A must be bigger than B and not an even number in another rule'); + it.todo('2 seperate fields with rules failing'); + }); + + describe('Derive values', () => { + it.todo('field_b is field_a * 2'); + }); }); From 503b22524958e20d74ef33b35e59589f73baff72 Mon Sep 17 00:00:00 2001 From: brennj Date: Tue, 4 Jul 2023 14:02:29 +0200 Subject: [PATCH 13/69] chore: proper name on the validations --- src/jsonLogic.js | 5 +++-- src/yupSchema.js | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/jsonLogic.js b/src/jsonLogic.js index 39d0b34eb..e4fb9504e 100644 --- a/src/jsonLogic.js +++ b/src/jsonLogic.js @@ -1,9 +1,10 @@ import jsonLogic from 'json-logic-js'; -export function yupSchemaWithCustomJSONLogic(validation) { +export function yupSchemaWithCustomJSONLogic(field, validation, index) { + console.log(field); return (yupSchema) => yupSchema.test( - 'randomName', + `${field.name}-validation-${index}`, validation.errorMessage ?? 'This field is invalid.', (_, { parent }) => { return jsonLogic.apply(validation.rule, parent); diff --git a/src/yupSchema.js b/src/yupSchema.js index 796c8d6d2..86d743152 100644 --- a/src/yupSchema.js +++ b/src/yupSchema.js @@ -337,8 +337,8 @@ export function buildYupSchema(field, config) { } if (propertyFields.validations) { - propertyFields.validations.forEach((rule) => - validators.push(yupSchemaWithCustomJSONLogic(rule)) + propertyFields.validations.forEach((validation, index) => + validators.push(yupSchemaWithCustomJSONLogic(field, validation, index)) ); } From c4034db3012580ea98c482a7e1a4871e51091d1c Mon Sep 17 00:00:00 2001 From: brennj Date: Tue, 4 Jul 2023 14:03:20 +0200 Subject: [PATCH 14/69] chore: remove console.log --- src/jsonLogic.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/jsonLogic.js b/src/jsonLogic.js index e4fb9504e..2d17e7974 100644 --- a/src/jsonLogic.js +++ b/src/jsonLogic.js @@ -1,7 +1,6 @@ import jsonLogic from 'json-logic-js'; export function yupSchemaWithCustomJSONLogic(field, validation, index) { - console.log(field); return (yupSchema) => yupSchema.test( `${field.name}-validation-${index}`, From 70cbaa92aa475f99901c6486f106cdb76663ab93 Mon Sep 17 00:00:00 2001 From: brennj Date: Tue, 4 Jul 2023 14:10:21 +0200 Subject: [PATCH 15/69] chore: more Arithmetic --- src/tests/jsonLogic.test.js | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/src/tests/jsonLogic.test.js b/src/tests/jsonLogic.test.js index 690976533..2751c5fb8 100644 --- a/src/tests/jsonLogic.test.js +++ b/src/tests/jsonLogic.test.js @@ -58,8 +58,32 @@ describe('cross-value validations', () => { }); describe('Arithmetic: +, -, *, /', () => { - it.todo('multiple: field_a > field_b * 2'); - it.todo('divide: field_a > field_b / 2'); + it('multiple: field_a > field_b * 2', () => { + const { handleValidation } = createHeadlessForm( + createSchemaWithRuleOnFieldA({ + errorMessage: 'Field A must be at least twice as big as field b', + rule: { '>': [{ var: 'field_a' }, { '*': [{ var: 'field_b' }, 2] }] }, + }), + { strictInputType: false } + ); + const { formErrors } = handleValidation({ field_a: 1, field_b: 4 }); + expect(formErrors.field_a).toEqual('Field A must be at least twice as big as field b'); + expect(handleValidation({ field_a: 3, field_b: 1 }).formErrors).toEqual(undefined); + }); + + it('divide: field_a > field_b / 2', () => { + const { handleValidation } = createHeadlessForm( + createSchemaWithRuleOnFieldA({ + errorMessage: 'Field A must be greater than field_b / 2', + rule: { '>': [{ var: 'field_a' }, { '/': [{ var: 'field_b' }, 2] }] }, + }), + { strictInputType: false } + ); + const { formErrors } = handleValidation({ field_a: 2, field_b: 4 }); + expect(formErrors.field_a).toEqual('Field A must be greater than field_b / 2'); + expect(handleValidation({ field_a: 3, field_b: 5 }).formErrors).toEqual(undefined); + }); + it.todo('sum: field_a > field_b + field_c'); // eg salary is bigger than X and Y together. }); From f87c34f19c978efcda2ad885ec4dbdffde9ac8bc Mon Sep 17 00:00:00 2001 From: brennj Date: Tue, 4 Jul 2023 14:29:30 +0200 Subject: [PATCH 16/69] boolean logic for ands and ors --- src/tests/jsonLogic.test.js | 143 +++++++++++++++++++++++++++++------- 1 file changed, 118 insertions(+), 25 deletions(-) diff --git a/src/tests/jsonLogic.test.js b/src/tests/jsonLogic.test.js index 2751c5fb8..4a52071ff 100644 --- a/src/tests/jsonLogic.test.js +++ b/src/tests/jsonLogic.test.js @@ -1,11 +1,11 @@ import { createHeadlessForm } from '../createHeadlessForm'; -function createSchemaWithRuleOnFieldA(rule) { +function createSchemaWithRulesOnFieldA(rules) { return { properties: { field_a: { type: 'number', - 'x-jsf-validations': [rule], + 'x-jsf-validations': rules, }, field_b: { type: 'number', @@ -15,14 +15,34 @@ function createSchemaWithRuleOnFieldA(rule) { }; } +function createSchemaWithThreePropertiesWithRuleOnFieldA(rules) { + return { + properties: { + field_a: { + type: 'number', + 'x-jsf-validations': rules, + }, + field_b: { + type: 'number', + }, + field_c: { + type: 'number', + }, + }, + required: ['field_a', 'field_b', 'field_c'], + }; +} + describe('cross-value validations', () => { describe('Relative: <, >, =', () => { it('bigger: field_a > field_b', () => { const { handleValidation } = createHeadlessForm( - createSchemaWithRuleOnFieldA({ - errorMessage: 'Field A must be bigger than field B', - rule: { '>': [{ var: 'field_a' }, { var: 'field_b' }] }, - }), + createSchemaWithRulesOnFieldA([ + { + errorMessage: 'Field A must be bigger than field B', + rule: { '>': [{ var: 'field_a' }, { var: 'field_b' }] }, + }, + ]), { strictInputType: false } ); const { formErrors } = handleValidation({ field_a: 1, field_b: 2 }); @@ -32,10 +52,12 @@ describe('cross-value validations', () => { it('smaller: field_a < field_b', () => { const { handleValidation } = createHeadlessForm( - createSchemaWithRuleOnFieldA({ - errorMessage: 'Field A must be smaller than field B', - rule: { '<': [{ var: 'field_a' }, { var: 'field_b' }] }, - }), + createSchemaWithRulesOnFieldA([ + { + errorMessage: 'Field A must be smaller than field B', + rule: { '<': [{ var: 'field_a' }, { var: 'field_b' }] }, + }, + ]), { strictInputType: false } ); const { formErrors } = handleValidation({ field_a: 2, field_b: 2 }); @@ -45,10 +67,12 @@ describe('cross-value validations', () => { it('equal: field_a = field_b', () => { const { handleValidation } = createHeadlessForm( - createSchemaWithRuleOnFieldA({ - errorMessage: 'Field A must equal field B', - rule: { '==': [{ var: 'field_a' }, { var: 'field_b' }] }, - }), + createSchemaWithRulesOnFieldA([ + { + errorMessage: 'Field A must equal field B', + rule: { '==': [{ var: 'field_a' }, { var: 'field_b' }] }, + }, + ]), { strictInputType: false } ); const { formErrors } = handleValidation({ field_a: 3, field_b: 2 }); @@ -60,10 +84,12 @@ describe('cross-value validations', () => { describe('Arithmetic: +, -, *, /', () => { it('multiple: field_a > field_b * 2', () => { const { handleValidation } = createHeadlessForm( - createSchemaWithRuleOnFieldA({ - errorMessage: 'Field A must be at least twice as big as field b', - rule: { '>': [{ var: 'field_a' }, { '*': [{ var: 'field_b' }, 2] }] }, - }), + createSchemaWithRulesOnFieldA([ + { + errorMessage: 'Field A must be at least twice as big as field b', + rule: { '>': [{ var: 'field_a' }, { '*': [{ var: 'field_b' }, 2] }] }, + }, + ]), { strictInputType: false } ); const { formErrors } = handleValidation({ field_a: 1, field_b: 4 }); @@ -73,10 +99,12 @@ describe('cross-value validations', () => { it('divide: field_a > field_b / 2', () => { const { handleValidation } = createHeadlessForm( - createSchemaWithRuleOnFieldA({ - errorMessage: 'Field A must be greater than field_b / 2', - rule: { '>': [{ var: 'field_a' }, { '/': [{ var: 'field_b' }, 2] }] }, - }), + createSchemaWithRulesOnFieldA([ + { + errorMessage: 'Field A must be greater than field_b / 2', + rule: { '>': [{ var: 'field_a' }, { '/': [{ var: 'field_b' }, 2] }] }, + }, + ]), { strictInputType: false } ); const { formErrors } = handleValidation({ field_a: 2, field_b: 4 }); @@ -84,12 +112,77 @@ describe('cross-value validations', () => { expect(handleValidation({ field_a: 3, field_b: 5 }).formErrors).toEqual(undefined); }); - it.todo('sum: field_a > field_b + field_c'); // eg salary is bigger than X and Y together. + it('sum: field_a > field_b + field_c', () => { + const schema = createSchemaWithThreePropertiesWithRuleOnFieldA([ + { + errorMessage: 'Field A must be greater than field_b and field_b added together', + rule: { + '>': [{ var: 'field_a' }, { '+': [{ var: 'field_b' }, { var: 'field_c' }] }], + }, + }, + ]); + const { handleValidation } = createHeadlessForm(schema, { strictInputType: false }); + const { formErrors } = handleValidation({ field_a: 0, field_b: 1, field_c: 2 }); + expect(formErrors.field_a).toEqual( + 'Field A must be greater than field_b and field_b added together' + ); + expect(handleValidation({ field_a: 4, field_b: 1, field_c: 2 }).formErrors).toEqual( + undefined + ); + }); }); describe('Logical: ||, &&', () => { - it.todo('AND: field_a > (field_b AND field_c)'); - it.todo('OR: field_a > (field_b OR field_c)'); + it('AND: field_a > field_b && field_a > field_c (implicit with multiple rules in a single field)', () => { + const schema = createSchemaWithThreePropertiesWithRuleOnFieldA([ + { + errorMessage: 'Field A must be greater than field_b', + rule: { + '>': [{ var: 'field_a' }, { var: 'field_b' }], + }, + }, + { + errorMessage: 'Field A must be greater than field_c', + rule: { + '>': [{ var: 'field_a' }, { var: 'field_c' }], + }, + }, + ]); + const { handleValidation } = createHeadlessForm(schema, { strictInputType: false }); + expect(handleValidation({ field_a: 1, field_b: 10, field_c: 0 }).formErrors.field_a).toEqual( + 'Field A must be greater than field_b' + ); + expect(handleValidation({ field_a: 1, field_b: 0, field_c: 10 }).formErrors.field_a).toEqual( + 'Field A must be greater than field_c' + ); + expect(handleValidation({ field_a: 10, field_b: 5, field_c: 5 }).formErrors).toEqual( + undefined + ); + }); + + it('OR: field_a > field_b or field_a > field_c', () => { + const schema = createSchemaWithThreePropertiesWithRuleOnFieldA([ + { + errorMessage: 'Field A must be greater than field_b or field_c', + rule: { + or: [ + { '>': [{ var: 'field_a' }, { var: 'field_b' }] }, + { '>': [{ var: 'field_a' }, { var: 'field_c' }] }, + ], + }, + }, + ]); + const { handleValidation } = createHeadlessForm(schema, { strictInputType: false }); + expect(handleValidation({ field_a: 0, field_b: 10, field_c: 10 }).formErrors.field_a).toEqual( + 'Field A must be greater than field_b or field_c' + ); + expect(handleValidation({ field_a: 1, field_b: 0, field_c: 10 }).formErrors).toEqual( + undefined + ); + expect(handleValidation({ field_a: 10, field_b: 5, field_c: 5 }).formErrors).toEqual( + undefined + ); + }); }); describe('Conditionals', () => { From 68bf668440b999ccaaea988a301d47a6a36e0fe1 Mon Sep 17 00:00:00 2001 From: brennj Date: Tue, 4 Jul 2023 16:30:26 +0200 Subject: [PATCH 17/69] chore: use object over array --- src/jsonLogic.js | 4 +-- src/tests/jsonLogic.test.js | 50 ++++++++++++++++++------------------- src/yupSchema.js | 4 +-- 3 files changed, 29 insertions(+), 29 deletions(-) diff --git a/src/jsonLogic.js b/src/jsonLogic.js index 2d17e7974..0a148ee1b 100644 --- a/src/jsonLogic.js +++ b/src/jsonLogic.js @@ -1,9 +1,9 @@ import jsonLogic from 'json-logic-js'; -export function yupSchemaWithCustomJSONLogic(field, validation, index) { +export function yupSchemaWithCustomJSONLogic(field, validation, id) { return (yupSchema) => yupSchema.test( - `${field.name}-validation-${index}`, + `${field.name}-validation-${id}`, validation.errorMessage ?? 'This field is invalid.', (_, { parent }) => { return jsonLogic.apply(validation.rule, parent); diff --git a/src/tests/jsonLogic.test.js b/src/tests/jsonLogic.test.js index 4a52071ff..abbf2e740 100644 --- a/src/tests/jsonLogic.test.js +++ b/src/tests/jsonLogic.test.js @@ -37,12 +37,12 @@ describe('cross-value validations', () => { describe('Relative: <, >, =', () => { it('bigger: field_a > field_b', () => { const { handleValidation } = createHeadlessForm( - createSchemaWithRulesOnFieldA([ - { + createSchemaWithRulesOnFieldA({ + a_greater_than_b: { errorMessage: 'Field A must be bigger than field B', rule: { '>': [{ var: 'field_a' }, { var: 'field_b' }] }, }, - ]), + }), { strictInputType: false } ); const { formErrors } = handleValidation({ field_a: 1, field_b: 2 }); @@ -52,12 +52,12 @@ describe('cross-value validations', () => { it('smaller: field_a < field_b', () => { const { handleValidation } = createHeadlessForm( - createSchemaWithRulesOnFieldA([ - { + createSchemaWithRulesOnFieldA({ + a_less_than_b: { errorMessage: 'Field A must be smaller than field B', rule: { '<': [{ var: 'field_a' }, { var: 'field_b' }] }, }, - ]), + }), { strictInputType: false } ); const { formErrors } = handleValidation({ field_a: 2, field_b: 2 }); @@ -67,12 +67,12 @@ describe('cross-value validations', () => { it('equal: field_a = field_b', () => { const { handleValidation } = createHeadlessForm( - createSchemaWithRulesOnFieldA([ - { + createSchemaWithRulesOnFieldA({ + a_equals_b: { errorMessage: 'Field A must equal field B', rule: { '==': [{ var: 'field_a' }, { var: 'field_b' }] }, }, - ]), + }), { strictInputType: false } ); const { formErrors } = handleValidation({ field_a: 3, field_b: 2 }); @@ -84,12 +84,12 @@ describe('cross-value validations', () => { describe('Arithmetic: +, -, *, /', () => { it('multiple: field_a > field_b * 2', () => { const { handleValidation } = createHeadlessForm( - createSchemaWithRulesOnFieldA([ - { + createSchemaWithRulesOnFieldA({ + a_greater_than_b_multiplied_by_2: { errorMessage: 'Field A must be at least twice as big as field b', rule: { '>': [{ var: 'field_a' }, { '*': [{ var: 'field_b' }, 2] }] }, }, - ]), + }), { strictInputType: false } ); const { formErrors } = handleValidation({ field_a: 1, field_b: 4 }); @@ -99,12 +99,12 @@ describe('cross-value validations', () => { it('divide: field_a > field_b / 2', () => { const { handleValidation } = createHeadlessForm( - createSchemaWithRulesOnFieldA([ - { + createSchemaWithRulesOnFieldA({ + a_greater_than_b_divided_by_2: { errorMessage: 'Field A must be greater than field_b / 2', rule: { '>': [{ var: 'field_a' }, { '/': [{ var: 'field_b' }, 2] }] }, }, - ]), + }), { strictInputType: false } ); const { formErrors } = handleValidation({ field_a: 2, field_b: 4 }); @@ -113,14 +113,14 @@ describe('cross-value validations', () => { }); it('sum: field_a > field_b + field_c', () => { - const schema = createSchemaWithThreePropertiesWithRuleOnFieldA([ - { + const schema = createSchemaWithThreePropertiesWithRuleOnFieldA({ + a_is_greater_than_b_plus_c: { errorMessage: 'Field A must be greater than field_b and field_b added together', rule: { '>': [{ var: 'field_a' }, { '+': [{ var: 'field_b' }, { var: 'field_c' }] }], }, }, - ]); + }); const { handleValidation } = createHeadlessForm(schema, { strictInputType: false }); const { formErrors } = handleValidation({ field_a: 0, field_b: 1, field_c: 2 }); expect(formErrors.field_a).toEqual( @@ -134,20 +134,20 @@ describe('cross-value validations', () => { describe('Logical: ||, &&', () => { it('AND: field_a > field_b && field_a > field_c (implicit with multiple rules in a single field)', () => { - const schema = createSchemaWithThreePropertiesWithRuleOnFieldA([ - { + const schema = createSchemaWithThreePropertiesWithRuleOnFieldA({ + a_is_greater_than_b: { errorMessage: 'Field A must be greater than field_b', rule: { '>': [{ var: 'field_a' }, { var: 'field_b' }], }, }, - { + a_is_greater_than_c: { errorMessage: 'Field A must be greater than field_c', rule: { '>': [{ var: 'field_a' }, { var: 'field_c' }], }, }, - ]); + }); const { handleValidation } = createHeadlessForm(schema, { strictInputType: false }); expect(handleValidation({ field_a: 1, field_b: 10, field_c: 0 }).formErrors.field_a).toEqual( 'Field A must be greater than field_b' @@ -161,8 +161,8 @@ describe('cross-value validations', () => { }); it('OR: field_a > field_b or field_a > field_c', () => { - const schema = createSchemaWithThreePropertiesWithRuleOnFieldA([ - { + const schema = createSchemaWithThreePropertiesWithRuleOnFieldA({ + field_a_is_greater_than_b_or_c: { errorMessage: 'Field A must be greater than field_b or field_c', rule: { or: [ @@ -171,7 +171,7 @@ describe('cross-value validations', () => { ], }, }, - ]); + }); const { handleValidation } = createHeadlessForm(schema, { strictInputType: false }); expect(handleValidation({ field_a: 0, field_b: 10, field_c: 10 }).formErrors.field_a).toEqual( 'Field A must be greater than field_b or field_c' diff --git a/src/yupSchema.js b/src/yupSchema.js index 86d743152..d21b6be7f 100644 --- a/src/yupSchema.js +++ b/src/yupSchema.js @@ -337,8 +337,8 @@ export function buildYupSchema(field, config) { } if (propertyFields.validations) { - propertyFields.validations.forEach((validation, index) => - validators.push(yupSchemaWithCustomJSONLogic(field, validation, index)) + Object.entries(propertyFields.validations).forEach(([id, validation]) => + validators.push(yupSchemaWithCustomJSONLogic(field, validation, id)) ); } From bcc1d6b86297818d5eaf0cd763049b915a81b8df Mon Sep 17 00:00:00 2001 From: brennj Date: Tue, 4 Jul 2023 16:37:56 +0200 Subject: [PATCH 18/69] chore: think of some further test cases --- src/tests/jsonLogic.test.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/tests/jsonLogic.test.js b/src/tests/jsonLogic.test.js index abbf2e740..92a673d22 100644 --- a/src/tests/jsonLogic.test.js +++ b/src/tests/jsonLogic.test.js @@ -197,4 +197,16 @@ describe('cross-value validations', () => { describe('Derive values', () => { it.todo('field_b is field_a * 2'); }); + + describe('Nested fieldsets', () => { + it.todo('Does everything above work when the field is nested'); + it.todo('Validate a field and a nested field together'); + it.todo('compute a nested field attribute'); + }); + + describe('Arrays', () => { + it.todo('How will this even work?'); + it.todo('What do I need to do when i need to validate all items'); + it.todo('What do i need to do when i need to validate a specific array item'); + }); }); From b032c1b8d5c264005ce29686e1a68631328a8d12 Mon Sep 17 00:00:00 2001 From: brennj Date: Wed, 5 Jul 2023 11:35:54 +0200 Subject: [PATCH 19/69] chore: validationMap kinda there --- src/createHeadlessForm.js | 11 +++++-- src/helpers.js | 61 ++++++++++++++++++++++++++----------- src/jsonLogic.js | 23 ++++++++++++++ src/tests/jsonLogic.test.js | 45 ++++++++++++++++++++++++++- 4 files changed, 119 insertions(+), 21 deletions(-) diff --git a/src/createHeadlessForm.js b/src/createHeadlessForm.js index 3bd7e7332..91fd7e5b7 100644 --- a/src/createHeadlessForm.js +++ b/src/createHeadlessForm.js @@ -24,6 +24,7 @@ import { getInputType, } from './internals/fields'; import { pickXKey } from './internals/helpers'; +import { getValidationsFromJSONSchema } from './jsonLogic'; import { buildYupSchema } from './yupSchema'; // Some type definitions (to be migrated into .d.ts file or TS Interfaces) @@ -324,10 +325,16 @@ export function createHeadlessForm(jsonSchema, customConfig = {}) { try { const fields = getFieldsFromJSONSchema(jsonSchema, config); + const validations = getValidationsFromJSONSchema(jsonSchema, config.initialValues); - const handleValidation = handleValuesChange(fields, jsonSchema, config); + const handleValidation = handleValuesChange(fields, jsonSchema, config, validations); - updateFieldsProperties(fields, getPrefillValues(fields, config.initialValues), jsonSchema); + updateFieldsProperties( + fields, + getPrefillValues(fields, config.initialValues), + jsonSchema, + validations + ); return { fields, diff --git a/src/helpers.js b/src/helpers.js index 94d6bc28f..141fc5bf5 100644 --- a/src/helpers.js +++ b/src/helpers.js @@ -68,8 +68,8 @@ function compareFormValueWithSchemaValue(formValue, schemaValue) { * @param {Object} formValues - form state * @returns {Boolean} */ -function checkIfConditionMatches(node, formValues, formFields) { - return Object.keys(node.if.properties).every((name) => { +function checkIfConditionMatches(node, formValues, formFields, validations) { + const propertiesMatch = Object.keys(node.if.properties ?? {}).every((name) => { const currentProperty = node.if.properties[name]; const value = formValues[name]; const hasEmptyValue = @@ -120,6 +120,16 @@ function checkIfConditionMatches(node, formValues, formFields) { value ); }); + + const validationsMatch = Object.entries(node.if['x-jsf-validations'] ?? {}).every( + ([name, property]) => { + const currentValue = validations.evaluateRule(name, formValues); + if (Object.hasOwn(property, 'const') && currentValue === property.const) return true; + return false; + } + ); + + return propertiesMatch && validationsMatch; } /** @@ -305,7 +315,7 @@ function updateField(field, requiredFields, node, formValues) { * @param {Set} accRequired - set of required field names gathered by traversing the tree * @returns {Object} */ -function processNode(node, formValues, formFields, accRequired = new Set()) { +function processNode({ node, formValues, formFields, accRequired = new Set(), validations }) { // Set initial required fields const requiredFields = new Set(accRequired); @@ -322,25 +332,27 @@ function processNode(node, formValues, formFields, accRequired = new Set()) { }); if (node.if) { - const matchesCondition = checkIfConditionMatches(node, formValues, formFields); + const matchesCondition = checkIfConditionMatches(node, formValues, formFields, validations); // BUG HERE (unreleated) - what if it matches but doesn't has a then, // it should do nothing, but instead it jumps to node.else when it shouldn't. if (matchesCondition && node.then) { - const { required: branchRequired } = processNode( - node.then, + const { required: branchRequired } = processNode({ + node: node.then, formValues, formFields, - requiredFields - ); + accRequired: requiredFields, + validations, + }); branchRequired.forEach((field) => requiredFields.add(field)); } else if (node.else) { - const { required: branchRequired } = processNode( - node.else, + const { required: branchRequired } = processNode({ + node: node.else, formValues, formFields, - requiredFields - ); + accRequired: requiredFields, + validations, + }); branchRequired.forEach((field) => requiredFields.add(field)); } } @@ -361,7 +373,15 @@ function processNode(node, formValues, formFields, accRequired = new Set()) { if (node.allOf) { node.allOf - .map((allOfNode) => processNode(allOfNode, formValues, formFields, requiredFields)) + .map((allOfNode) => + processNode({ + node: allOfNode, + formValues, + formFields, + accRequired: requiredFields, + validations, + }) + ) .forEach(({ required: allOfItemRequired }) => { allOfItemRequired.forEach(requiredFields.add, requiredFields); }); @@ -372,7 +392,12 @@ function processNode(node, formValues, formFields, accRequired = new Set()) { const inputType = getInputType(nestedNode); if (inputType === supportedTypes.FIELDSET) { // It's a fieldset, which might contain scoped conditions - processNode(nestedNode, formValues[name] || {}, getField(name, formFields).fields); + processNode({ + node: nestedNode, + formValues: formValues[name] || {}, + formFields: getField(name, formFields).fields, + validations, + }); } }); } @@ -407,11 +432,11 @@ function clearValuesIfNotVisible(fields, formValues) { * @param {Object} formValues - current values of the form * @param {Object} jsonSchema - JSON schema object */ -export function updateFieldsProperties(fields, formValues, jsonSchema) { +export function updateFieldsProperties(fields, formValues, jsonSchema, validations) { if (!jsonSchema?.properties) { return; } - processNode(jsonSchema, formValues, fields); + processNode({ node: jsonSchema, formValues, formFields: fields, validations }); clearValuesIfNotVisible(fields, formValues); } @@ -577,8 +602,8 @@ export function yupToFormErrors(yupError) { * @param {JsfConfig} config - jsf config * @returns {Function(values: Object): { YupError: YupObject, formErrors: Object }} Callback that returns Yup errors */ -export const handleValuesChange = (fields, jsonSchema, config) => (values) => { - updateFieldsProperties(fields, values, jsonSchema); +export const handleValuesChange = (fields, jsonSchema, config, validations) => (values) => { + updateFieldsProperties(fields, values, jsonSchema, validations); const lazySchema = lazy(() => buildCompleteYupSchema(fields, config)); let errors; diff --git a/src/jsonLogic.js b/src/jsonLogic.js index 0a148ee1b..0571f1520 100644 --- a/src/jsonLogic.js +++ b/src/jsonLogic.js @@ -1,5 +1,28 @@ import jsonLogic from 'json-logic-js'; +/** + * Parses the JSON schema to extract the advanced validation logic and returns a set of functionality to check the current status of said rules. + * @param {Object} schema - JSON schema node + * @param {Object} initialValues - form state + * @returns {Object} + */ +export function getValidationsFromJSONSchema(schema, initialValues) { + const ruleMap = new Map(); + + const validationObject = Object.entries(schema?.['x-jsf-validations'] ?? {}); + validationObject.forEach(([id, { rule }]) => { + ruleMap.set(id, { rule, evaluation: jsonLogic.apply(rule, initialValues) }); + }); + + return { + ruleMap, + evaluateRule(id, values) { + const validation = ruleMap.get(id); + return jsonLogic.apply(validation.rule, values); + }, + }; +} + export function yupSchemaWithCustomJSONLogic(field, validation, id) { return (yupSchema) => yupSchema.test( diff --git a/src/tests/jsonLogic.test.js b/src/tests/jsonLogic.test.js index 92a673d22..f68c312c7 100644 --- a/src/tests/jsonLogic.test.js +++ b/src/tests/jsonLogic.test.js @@ -186,7 +186,50 @@ describe('cross-value validations', () => { }); describe('Conditionals', () => { - it.todo('when field_a > field_b, show field_c'); + const schema = { + properties: { + field_a: { + type: 'number', + }, + field_b: { + type: 'number', + }, + field_c: { + type: 'number', + }, + }, + required: ['field_a', 'field_b'], + allOf: [ + { + if: { + 'x-jsf-validations': { + require_c: { + const: true, + }, + }, + }, + then: { + required: ['field_c'], + }, + else: { + properties: { + field_c: false, + }, + }, + }, + ], + 'x-jsf-validations': { + require_c: { + rule: { + '>': [{ var: 'field_a' }, { var: 'field_b' }], + }, + }, + }, + }; + + it('when field_a > field_b, show field_c', () => { + createHeadlessForm(schema, { strictInputType: false }); + }); }); describe('Multiple validations', () => { From ccbe54a390735c80bf4e0101203c27cfb09a62b9 Mon Sep 17 00:00:00 2001 From: brennj Date: Wed, 5 Jul 2023 12:24:34 +0200 Subject: [PATCH 20/69] chore: it kinda works! --- src/jsonLogic.js | 9 ++++++++- src/tests/jsonLogic.test.js | 20 ++++++++++++++++++-- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/src/jsonLogic.js b/src/jsonLogic.js index 0571f1520..9b63a49b3 100644 --- a/src/jsonLogic.js +++ b/src/jsonLogic.js @@ -18,11 +18,18 @@ export function getValidationsFromJSONSchema(schema, initialValues) { ruleMap, evaluateRule(id, values) { const validation = ruleMap.get(id); - return jsonLogic.apply(validation.rule, values); + const answer = jsonLogic.apply(validation.rule, clean(values)); + return answer; }, }; } +function clean(values) { + return Object.entries(values).reduce((prev, [key, value]) => { + return { ...prev, [key]: value === undefined ? null : value }; + }, {}); +} + export function yupSchemaWithCustomJSONLogic(field, validation, id) { return (yupSchema) => yupSchema.test( diff --git a/src/tests/jsonLogic.test.js b/src/tests/jsonLogic.test.js index f68c312c7..973ffd556 100644 --- a/src/tests/jsonLogic.test.js +++ b/src/tests/jsonLogic.test.js @@ -221,14 +221,30 @@ describe('cross-value validations', () => { 'x-jsf-validations': { require_c: { rule: { - '>': [{ var: 'field_a' }, { var: 'field_b' }], + and: [ + { '!==': [{ var: 'field_b' }, null] }, + { '!==': [{ var: 'field_a' }, null] }, + { '>': [{ var: 'field_a' }, { var: 'field_b' }] }, + ], }, }, }, }; it('when field_a > field_b, show field_c', () => { - createHeadlessForm(schema, { strictInputType: false }); + const { fields, handleValidation } = createHeadlessForm(schema, { strictInputType: false }); + expect(fields.find((i) => i.name === 'field_c').isVisible).toEqual(false); + + expect(handleValidation({ field_a: 1, field_b: 3 }).formErrors).toEqual(undefined); + expect(handleValidation({ field_a: 1, field_b: null }).formErrors).toEqual({ + field_b: 'Required field', + }); + expect(handleValidation({ field_a: 10, field_b: 3 }).formErrors).toEqual({ + field_c: 'Required field', + }); + expect(handleValidation({ field_a: 10, field_b: 3, field_c: 0 }).formErrors).toEqual( + undefined + ); }); }); From 8b9570700e656d2e83d98cdfd00e006d56128108 Mon Sep 17 00:00:00 2001 From: brennj Date: Wed, 5 Jul 2023 13:15:00 +0200 Subject: [PATCH 21/69] chore: current work --- src/tests/jsonLogic.test.js | 65 +++++++++++++++++-------------------- 1 file changed, 29 insertions(+), 36 deletions(-) diff --git a/src/tests/jsonLogic.test.js b/src/tests/jsonLogic.test.js index 973ffd556..02111bffc 100644 --- a/src/tests/jsonLogic.test.js +++ b/src/tests/jsonLogic.test.js @@ -36,45 +36,39 @@ function createSchemaWithThreePropertiesWithRuleOnFieldA(rules) { describe('cross-value validations', () => { describe('Relative: <, >, =', () => { it('bigger: field_a > field_b', () => { - const { handleValidation } = createHeadlessForm( - createSchemaWithRulesOnFieldA({ - a_greater_than_b: { - errorMessage: 'Field A must be bigger than field B', - rule: { '>': [{ var: 'field_a' }, { var: 'field_b' }] }, - }, - }), - { strictInputType: false } - ); + const schema = createSchemaWithRulesOnFieldA({ + a_greater_than_b: { + errorMessage: 'Field A must be bigger than field B', + rule: { '>': [{ var: 'field_a' }, { var: 'field_b' }] }, + }, + }); + const { handleValidation } = createHeadlessForm(schema, { strictInputType: false }); const { formErrors } = handleValidation({ field_a: 1, field_b: 2 }); expect(formErrors.field_a).toEqual('Field A must be bigger than field B'); expect(handleValidation({ field_a: 2, field_b: 0 }).formErrors).toEqual(undefined); }); it('smaller: field_a < field_b', () => { - const { handleValidation } = createHeadlessForm( - createSchemaWithRulesOnFieldA({ - a_less_than_b: { - errorMessage: 'Field A must be smaller than field B', - rule: { '<': [{ var: 'field_a' }, { var: 'field_b' }] }, - }, - }), - { strictInputType: false } - ); + const schema = createSchemaWithRulesOnFieldA({ + a_less_than_b: { + errorMessage: 'Field A must be smaller than field B', + rule: { '<': [{ var: 'field_a' }, { var: 'field_b' }] }, + }, + }); + const { handleValidation } = createHeadlessForm(schema, { strictInputType: false }); const { formErrors } = handleValidation({ field_a: 2, field_b: 2 }); expect(formErrors.field_a).toEqual('Field A must be smaller than field B'); expect(handleValidation({ field_a: 0, field_b: 2 }).formErrors).toEqual(undefined); }); it('equal: field_a = field_b', () => { - const { handleValidation } = createHeadlessForm( - createSchemaWithRulesOnFieldA({ - a_equals_b: { - errorMessage: 'Field A must equal field B', - rule: { '==': [{ var: 'field_a' }, { var: 'field_b' }] }, - }, - }), - { strictInputType: false } - ); + const schema = createSchemaWithRulesOnFieldA({ + a_equals_b: { + errorMessage: 'Field A must equal field B', + rule: { '==': [{ var: 'field_a' }, { var: 'field_b' }] }, + }, + }); + const { handleValidation } = createHeadlessForm(schema, { strictInputType: false }); const { formErrors } = handleValidation({ field_a: 3, field_b: 2 }); expect(formErrors.field_a).toEqual('Field A must equal field B'); expect(handleValidation({ field_a: 2, field_b: 2 }).formErrors).toEqual(undefined); @@ -83,15 +77,14 @@ describe('cross-value validations', () => { describe('Arithmetic: +, -, *, /', () => { it('multiple: field_a > field_b * 2', () => { - const { handleValidation } = createHeadlessForm( - createSchemaWithRulesOnFieldA({ - a_greater_than_b_multiplied_by_2: { - errorMessage: 'Field A must be at least twice as big as field b', - rule: { '>': [{ var: 'field_a' }, { '*': [{ var: 'field_b' }, 2] }] }, - }, - }), - { strictInputType: false } - ); + const schema = createSchemaWithRulesOnFieldA({ + a_greater_than_b_multiplied_by_2: { + errorMessage: 'Field A must be at least twice as big as field b', + rule: { '>': [{ var: 'field_a' }, { '*': [{ var: 'field_b' }, 2] }] }, + }, + }); + const { handleValidation } = createHeadlessForm(schema, { strictInputType: false }); + const { formErrors } = handleValidation({ field_a: 1, field_b: 4 }); expect(formErrors.field_a).toEqual('Field A must be at least twice as big as field b'); expect(handleValidation({ field_a: 3, field_b: 1 }).formErrors).toEqual(undefined); From a17f4c73a8f013f553e09c4070e2c80204a7fa96 Mon Sep 17 00:00:00 2001 From: brennj Date: Wed, 5 Jul 2023 15:07:16 +0200 Subject: [PATCH 22/69] chore: looks like we might not need to evaluate initially for now --- src/createHeadlessForm.js | 2 +- src/jsonLogic.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/createHeadlessForm.js b/src/createHeadlessForm.js index 91fd7e5b7..ca2b02dda 100644 --- a/src/createHeadlessForm.js +++ b/src/createHeadlessForm.js @@ -325,7 +325,7 @@ export function createHeadlessForm(jsonSchema, customConfig = {}) { try { const fields = getFieldsFromJSONSchema(jsonSchema, config); - const validations = getValidationsFromJSONSchema(jsonSchema, config.initialValues); + const validations = getValidationsFromJSONSchema(jsonSchema); const handleValidation = handleValuesChange(fields, jsonSchema, config, validations); diff --git a/src/jsonLogic.js b/src/jsonLogic.js index 9b63a49b3..cba81f138 100644 --- a/src/jsonLogic.js +++ b/src/jsonLogic.js @@ -6,12 +6,12 @@ import jsonLogic from 'json-logic-js'; * @param {Object} initialValues - form state * @returns {Object} */ -export function getValidationsFromJSONSchema(schema, initialValues) { +export function getValidationsFromJSONSchema(schema) { const ruleMap = new Map(); const validationObject = Object.entries(schema?.['x-jsf-validations'] ?? {}); validationObject.forEach(([id, { rule }]) => { - ruleMap.set(id, { rule, evaluation: jsonLogic.apply(rule, initialValues) }); + ruleMap.set(id, { rule }); }); return { From 382a2f8485646750508e3d077f705e2d59b7bb25 Mon Sep 17 00:00:00 2001 From: brennj Date: Wed, 5 Jul 2023 15:22:38 +0200 Subject: [PATCH 23/69] chores: tests on conditional validations --- src/tests/jsonLogic.test.js | 208 +++++++++++++++++++++++++++++------- 1 file changed, 170 insertions(+), 38 deletions(-) diff --git a/src/tests/jsonLogic.test.js b/src/tests/jsonLogic.test.js index 02111bffc..c69d7853e 100644 --- a/src/tests/jsonLogic.test.js +++ b/src/tests/jsonLogic.test.js @@ -179,52 +179,52 @@ describe('cross-value validations', () => { }); describe('Conditionals', () => { - const schema = { - properties: { - field_a: { - type: 'number', - }, - field_b: { - type: 'number', - }, - field_c: { - type: 'number', + it('when field_a > field_b, show field_c', () => { + const schema = { + properties: { + field_a: { + type: 'number', + }, + field_b: { + type: 'number', + }, + field_c: { + type: 'number', + }, }, - }, - required: ['field_a', 'field_b'], - allOf: [ - { - if: { - 'x-jsf-validations': { - require_c: { - const: true, + required: ['field_a', 'field_b'], + allOf: [ + { + if: { + 'x-jsf-validations': { + require_c: { + const: true, + }, }, }, - }, - then: { - required: ['field_c'], - }, - else: { - properties: { - field_c: false, + then: { + required: ['field_c'], + }, + else: { + properties: { + field_c: false, + }, }, }, - }, - ], - 'x-jsf-validations': { - require_c: { - rule: { - and: [ - { '!==': [{ var: 'field_b' }, null] }, - { '!==': [{ var: 'field_a' }, null] }, - { '>': [{ var: 'field_a' }, { var: 'field_b' }] }, - ], + ], + 'x-jsf-validations': { + require_c: { + rule: { + and: [ + { '!==': [{ var: 'field_b' }, null] }, + { '!==': [{ var: 'field_a' }, null] }, + { '>': [{ var: 'field_a' }, { var: 'field_b' }] }, + ], + }, }, }, - }, - }; + }; - it('when field_a > field_b, show field_c', () => { const { fields, handleValidation } = createHeadlessForm(schema, { strictInputType: false }); expect(fields.find((i) => i.name === 'field_c').isVisible).toEqual(false); @@ -239,6 +239,138 @@ describe('cross-value validations', () => { undefined ); }); + + it('A schema with both a `x-jsf-validations` and `properties` check', () => { + const schema = { + properties: { + field_a: { + type: 'number', + }, + field_b: { + type: 'number', + }, + field_c: { + type: 'number', + }, + }, + required: ['field_a', 'field_b'], + allOf: [ + { + if: { + 'x-jsf-validations': { + require_c: { + const: true, + }, + }, + properties: { + field_a: { + const: 10, + }, + }, + }, + then: { + required: ['field_c'], + }, + else: { + properties: { + field_c: false, + }, + }, + }, + ], + 'x-jsf-validations': { + require_c: { + rule: { + and: [ + { '!==': [{ var: 'field_b' }, null] }, + { '!==': [{ var: 'field_a' }, null] }, + { '>': [{ var: 'field_a' }, { var: 'field_b' }] }, + ], + }, + }, + }, + }; + const { handleValidation } = createHeadlessForm(schema, { strictInputType: false }); + expect(handleValidation({ field_a: 1, field_b: 3 }).formErrors).toEqual(undefined); + expect(handleValidation({ field_a: 10, field_b: 3 }).formErrors).toEqual({ + field_c: 'Required field', + }); + expect(handleValidation({ field_a: 5, field_b: 3 }).formErrors).toEqual(undefined); + }); + + it('Conditionally apply a validation on a property depending on values', () => { + const schema = { + properties: { + field_a: { + type: 'number', + }, + field_b: { + type: 'number', + }, + field_c: { + type: 'number', + }, + }, + required: ['field_a', 'field_b'], + allOf: [ + { + if: { + 'x-jsf-validations': { + require_c: { + const: true, + }, + }, + }, + then: { + required: ['field_c'], + properties: { + field_c: { + description: 'I am a description!', + 'x-jsf-validations': { + c_must_be_large: { + errorMessage: 'Needs more numbers', + rule: { + '>': [{ var: 'field_c' }, 200], + }, + }, + }, + }, + }, + }, + else: { + properties: { + field_c: false, + }, + }, + }, + ], + 'x-jsf-validations': { + require_c: { + rule: { + and: [ + { '!==': [{ var: 'field_b' }, null] }, + { '!==': [{ var: 'field_a' }, null] }, + { '>': [{ var: 'field_a' }, { var: 'field_b' }] }, + ], + }, + }, + }, + }; + const { fields, handleValidation } = createHeadlessForm(schema, { strictInputType: false }); + const cField = fields.find((i) => i.name === 'field_c'); + expect(cField.isVisible).toEqual(false); + expect(cField.description).toEqual(undefined); + expect(handleValidation({ field_a: 10, field_b: 5 }).formErrors).toEqual({ + field_c: 'Required field', + }); + expect(handleValidation({ field_a: 10, field_b: 5, field_c: 0 }).formErrors).toEqual({ + field_c: 'Needs more numbers', + }); + expect(cField.description).toBe('I am a description!'); + expect(handleValidation({ field_a: 10, field_b: 5, field_c: 201 }).formErrors).toEqual( + undefined + ); + }); }); describe('Multiple validations', () => { From a3c27ccd7ae74c1b7c1af8be48cd25adf136506d Mon Sep 17 00:00:00 2001 From: brennj Date: Wed, 5 Jul 2023 15:42:18 +0200 Subject: [PATCH 24/69] chore: more tests --- src/tests/jsonLogic.test.js | 79 ++++++++++++++++++++++++++++++++++++- 1 file changed, 77 insertions(+), 2 deletions(-) diff --git a/src/tests/jsonLogic.test.js b/src/tests/jsonLogic.test.js index c69d7853e..33e971a2f 100644 --- a/src/tests/jsonLogic.test.js +++ b/src/tests/jsonLogic.test.js @@ -374,8 +374,83 @@ describe('cross-value validations', () => { }); describe('Multiple validations', () => { - it.todo('2 rules where A must be bigger than B and not an even number in another rule'); - it.todo('2 seperate fields with rules failing'); + it('2 rules where A must be bigger than B and not an even number in another rule', () => { + const schema = { + properties: { + field_a: { + type: 'number', + 'x-jsf-validations': { + a_bigger_than_b: { + errorMessage: 'A must be bigger than B', + rule: { + '>': [{ var: 'field_a' }, { var: 'field_b' }], + }, + }, + is_even_number: { + errorMessage: 'A must be even', + rule: { + '===': [{ '%': [{ var: 'field_a' }, 2] }, 0], + }, + }, + }, + }, + field_b: { + type: 'number', + }, + }, + required: ['field_a', 'field_b'], + }; + + const { handleValidation } = createHeadlessForm(schema, { strictInputType: false }); + expect(handleValidation({ field_a: 1 }).formErrors).toEqual({ + field_a: 'A must be even', + field_b: 'Required field', + }); + expect(handleValidation({ field_a: 1, field_b: 2 }).formErrors).toEqual({ + field_a: 'A must be bigger than B', + }); + expect(handleValidation({ field_a: 3, field_b: 2 }).formErrors).toEqual({ + field_a: 'A must be even', + }); + expect(handleValidation({ field_a: 4, field_b: 2 }).formErrors).toEqual(undefined); + }); + + it('2 seperate fields with rules failing', () => { + const schema = { + properties: { + field_a: { + type: 'number', + 'x-jsf-validations': { + a_bigger_than_b: { + errorMessage: 'A must be bigger than B', + rule: { + '>': [{ var: 'field_a' }, { var: 'field_b' }], + }, + }, + }, + }, + field_b: { + type: 'number', + 'x-jsf-validations': { + is_even_number: { + errorMessage: 'B must be even', + rule: { + '===': [{ '%': [{ var: 'field_b' }, 2] }, 0], + }, + }, + }, + }, + }, + required: ['field_a', 'field_b'], + }; + + const { handleValidation } = createHeadlessForm(schema, { strictInputType: false }); + expect(handleValidation({ field_a: 1, field_b: 3 }).formErrors).toEqual({ + field_a: 'A must be bigger than B', + field_b: 'B must be even', + }); + expect(handleValidation({ field_a: 4, field_b: 2 }).formErrors).toEqual(undefined); + }); }); describe('Derive values', () => { From e4099fb691a8cd7c548ac8dd43a612823eb41fe2 Mon Sep 17 00:00:00 2001 From: brennj Date: Wed, 5 Jul 2023 16:43:48 +0200 Subject: [PATCH 25/69] chore: computed attributes kinda seem to work --- src/createHeadlessForm.js | 6 +++++- src/helpers.js | 23 ++++++++++++++++++----- src/jsonLogic.js | 29 ++++++++++++++++++++++++++++- src/tests/jsonLogic.test.js | 32 +++++++++++++++++++++++++++++++- 4 files changed, 82 insertions(+), 8 deletions(-) diff --git a/src/createHeadlessForm.js b/src/createHeadlessForm.js index ca2b02dda..f08cc0e3f 100644 --- a/src/createHeadlessForm.js +++ b/src/createHeadlessForm.js @@ -24,7 +24,7 @@ import { getInputType, } from './internals/fields'; import { pickXKey } from './internals/helpers'; -import { getValidationsFromJSONSchema } from './jsonLogic'; +import { calculateComputedAttributes, getValidationsFromJSONSchema } from './jsonLogic'; import { buildYupSchema } from './yupSchema'; // Some type definitions (to be migrated into .d.ts file or TS Interfaces) @@ -236,6 +236,9 @@ function buildField(fieldParams, config, scopedJsonSchema) { customProperties ); + const caclulateComputedAttributes = + fieldParams.computedAttributes && calculateComputedAttributes(fieldParams); + const hasCustomValidations = !!customProperties && size(pick(customProperties, SUPPORTED_CUSTOM_VALIDATION_FIELD_PARAMS)) > 0; @@ -251,6 +254,7 @@ function buildField(fieldParams, config, scopedJsonSchema) { ...(hasCustomValidations && { calculateCustomValidationProperties: calculateCustomValidationPropertiesClosure, }), + ...(caclulateComputedAttributes && { caclulateComputedAttributes }), // field customization properties ...(customProperties && { fieldCustomization: customProperties }), // base schema diff --git a/src/helpers.js b/src/helpers.js index 141fc5bf5..df6e88543 100644 --- a/src/helpers.js +++ b/src/helpers.js @@ -238,7 +238,7 @@ export function getPrefillValues(fields, initialValues = {}) { * @param {Object} node - JSON-schema node * @returns */ -function updateField(field, requiredFields, node, formValues) { +function updateField(field, requiredFields, node, formValues, validations) { // If there was an error building the field, it might not exist in the form even though // it can be mentioned in the schema so we return early in that case if (!field) { @@ -299,6 +299,17 @@ function updateField(field, requiredFields, node, formValues) { ); updateValues(newFieldValues); } + + if (field.caclulateComputedAttributes) { + const computedFieldValues = field.caclulateComputedAttributes({ + field, + isRequired: fieldIsRequired, + node, + formValues, + validations, + }); + updateValues(computedFieldValues); + } } /** @@ -322,13 +333,13 @@ function processNode({ node, formValues, formFields, accRequired = new Set(), va // Go through the node properties definition and update each field accordingly Object.keys(node.properties ?? []).forEach((fieldName) => { const field = getField(fieldName, formFields); - updateField(field, requiredFields, node, formValues); + updateField(field, requiredFields, node, formValues, validations); }); // Update required fields based on the `required` property and mutate node if needed node.required?.forEach((fieldName) => { requiredFields.add(fieldName); - updateField(getField(fieldName, formFields), requiredFields, node, formValues); + updateField(getField(fieldName, formFields), requiredFields, node, formValues, validations); }); if (node.if) { @@ -366,7 +377,7 @@ function processNode({ node, formValues, formFields, accRequired = new Set(), va node.anyOf.forEach(({ required = [] }) => { required.forEach((fieldName) => { const field = getField(fieldName, formFields); - updateField(field, requiredFields, node, formValues); + updateField(field, requiredFields, node, formValues, validations); }); }); } @@ -500,6 +511,7 @@ export function extractParametersFromNode(schemaNode) { const presentation = pickXKey(schemaNode, 'presentation') ?? {}; const errorMessage = pickXKey(schemaNode, 'errorMessage') ?? {}; const validations = schemaNode['x-jsf-validations']; + const computedAttributes = schemaNode['x-jsf-computedAttributes']; const node = omit(schemaNode, ['x-jsf-presentation', 'presentation']); @@ -544,7 +556,8 @@ export function extractParametersFromNode(schemaNode) { // Handle [name].presentation ...presentation, - validations: validations, + validations, + computedAttributes, description: containsHTML(description) ? wrapWithSpan(description, { class: 'jsf-description', diff --git a/src/jsonLogic.js b/src/jsonLogic.js index cba81f138..5e02106a7 100644 --- a/src/jsonLogic.js +++ b/src/jsonLogic.js @@ -24,7 +24,7 @@ export function getValidationsFromJSONSchema(schema) { }; } -function clean(values) { +function clean(values = {}) { return Object.entries(values).reduce((prev, [key, value]) => { return { ...prev, [key]: value === undefined ? null : value }; }, {}); @@ -40,3 +40,30 @@ export function yupSchemaWithCustomJSONLogic(field, validation, id) { } ); } + +function replaceHandlebarsTemplates(string, validations, formValues) { + return string.replace(/\{\{([^{}]+)\}\}/g, (match, key) => { + return validations.evaluateRule(key.trim(), formValues); + }); +} + +export function calculateComputedAttributes(fieldParams) { + return ({ validations, field, formValues }) => { + const { computedAttributes } = fieldParams; + console.log(field); + return Object.fromEntries( + Object.entries(computedAttributes) + .map(([key, value]) => { + if (key === 'description') + return [key, replaceHandlebarsTemplates(value, validations, formValues)]; + if (key === 'title') { + return ['label', replaceHandlebarsTemplates(value, validations, formValues)]; + } + if (key === 'const' || key === 'value') + return [key, validations.evaluateRule(value, formValues)]; + return [key, null]; + }) + .filter(([, value]) => value !== null) + ); + }; +} diff --git a/src/tests/jsonLogic.test.js b/src/tests/jsonLogic.test.js index 33e971a2f..8a4aa7697 100644 --- a/src/tests/jsonLogic.test.js +++ b/src/tests/jsonLogic.test.js @@ -454,7 +454,37 @@ describe('cross-value validations', () => { }); describe('Derive values', () => { - it.todo('field_b is field_a * 2'); + it('field_b is field_a * 2', () => { + const schema = { + properties: { + field_a: { + type: 'number', + }, + field_b: { + type: 'number', + 'x-jsf-computedAttributes': { + description: + 'This field is 2 times bigger than field_a with value of {{a_times_two}}.', + }, + }, + }, + required: ['field_a', 'field_b'], + 'x-jsf-validations': { + a_times_two: { + rule: { + '*': [{ var: 'field_a' }, 2], + }, + }, + }, + }; + const { fields } = createHeadlessForm(schema, { + strictInputType: false, + initialValues: { field_a: 2 }, + }); + expect(fields.find((i) => i.name === 'field_b').description).toEqual( + 'This field is 2 times bigger than field_a with value of 4.' + ); + }); }); describe('Nested fieldsets', () => { From bb0269c448745192031a1e7a472651dc964accaf Mon Sep 17 00:00:00 2001 From: brennj Date: Thu, 6 Jul 2023 12:15:53 +0200 Subject: [PATCH 26/69] chore: everything fixed except conditionals --- src/createHeadlessForm.js | 12 ++--- src/helpers.js | 13 +++--- src/jsonLogic.js | 31 +++++++++---- src/tests/jsonLogic.test.js | 88 +++++++++++++++++++++---------------- src/yupSchema.js | 23 +++++----- 5 files changed, 98 insertions(+), 69 deletions(-) diff --git a/src/createHeadlessForm.js b/src/createHeadlessForm.js index f08cc0e3f..d8ebda1c8 100644 --- a/src/createHeadlessForm.js +++ b/src/createHeadlessForm.js @@ -223,11 +223,11 @@ function getComposeFunctionForField(fieldParams, hasCustomizations) { * @param {JsfConfig} config - parser config * @returns {Object} field object */ -function buildField(fieldParams, config, scopedJsonSchema) { +function buildField(fieldParams, config, scopedJsonSchema, validations) { const customProperties = getCustomPropertiesForField(fieldParams, config); const composeFn = getComposeFunctionForField(fieldParams, !!customProperties); - const yupSchema = buildYupSchema(fieldParams, config); + const yupSchema = buildYupSchema(fieldParams, config, validations); const calculateConditionalFieldsClosure = fieldParams.isDynamic && calculateConditionalProperties(fieldParams, customProperties); @@ -272,7 +272,7 @@ function buildField(fieldParams, config, scopedJsonSchema) { * @param {JsfConfig} config - JSON-schema-form config * @returns {ParserFields} ParserFields */ -function getFieldsFromJSONSchema(scopedJsonSchema, config) { +function getFieldsFromJSONSchema(scopedJsonSchema, config, validations) { if (!scopedJsonSchema) { // NOTE: other type of verifications might be needed. return []; @@ -304,11 +304,11 @@ function getFieldsFromJSONSchema(scopedJsonSchema, config) { addFieldText: fieldParams.addFieldText, }; - buildField(fieldParams, config, scopedJsonSchema).forEach((groupField) => { + buildField(fieldParams, config, scopedJsonSchema, validations).forEach((groupField) => { fields.push(groupField); }); } else { - fields.push(buildField(fieldParams, config, scopedJsonSchema)); + fields.push(buildField(fieldParams, config, scopedJsonSchema, validations)); } }); @@ -328,8 +328,8 @@ export function createHeadlessForm(jsonSchema, customConfig = {}) { }; try { - const fields = getFieldsFromJSONSchema(jsonSchema, config); const validations = getValidationsFromJSONSchema(jsonSchema); + const fields = getFieldsFromJSONSchema(jsonSchema, config, validations); const handleValidation = handleValuesChange(fields, jsonSchema, config, validations); diff --git a/src/helpers.js b/src/helpers.js index df6e88543..bda38b1d1 100644 --- a/src/helpers.js +++ b/src/helpers.js @@ -39,8 +39,8 @@ function getField(fieldName, fields) { * @param {any} value * @returns */ -function validateFieldSchema(field, value) { - const validator = buildYupSchema(field); +function validateFieldSchema(field, value, validations) { + const validator = buildYupSchema(field, undefined, validations); return validator().isValidSync(value); } @@ -117,7 +117,8 @@ function checkIfConditionMatches(node, formValues, formFields, validations) { inputType: field.inputType, required: true, }, - value + value, + validations ); }); @@ -510,7 +511,7 @@ export function extractParametersFromNode(schemaNode) { const presentation = pickXKey(schemaNode, 'presentation') ?? {}; const errorMessage = pickXKey(schemaNode, 'errorMessage') ?? {}; - const validations = schemaNode['x-jsf-validations']; + const requiredValidations = schemaNode['x-jsf-requiredValidations']; const computedAttributes = schemaNode['x-jsf-computedAttributes']; const node = omit(schemaNode, ['x-jsf-presentation', 'presentation']); @@ -556,7 +557,7 @@ export function extractParametersFromNode(schemaNode) { // Handle [name].presentation ...presentation, - validations, + requiredValidations, computedAttributes, description: containsHTML(description) ? wrapWithSpan(description, { @@ -618,7 +619,7 @@ export function yupToFormErrors(yupError) { export const handleValuesChange = (fields, jsonSchema, config, validations) => (values) => { updateFieldsProperties(fields, values, jsonSchema, validations); - const lazySchema = lazy(() => buildCompleteYupSchema(fields, config)); + const lazySchema = lazy(() => buildCompleteYupSchema(fields, config, validations)); let errors; try { diff --git a/src/jsonLogic.js b/src/jsonLogic.js index 5e02106a7..77b979beb 100644 --- a/src/jsonLogic.js +++ b/src/jsonLogic.js @@ -7,17 +7,30 @@ import jsonLogic from 'json-logic-js'; * @returns {Object} */ export function getValidationsFromJSONSchema(schema) { - const ruleMap = new Map(); + const validationMap = new Map(); + const computedValuesMap = new Map(); - const validationObject = Object.entries(schema?.['x-jsf-validations'] ?? {}); - validationObject.forEach(([id, { rule }]) => { - ruleMap.set(id, { rule }); + const logic = schema?.['x-jsf-logic'] ?? { + validations: {}, + computedValues: {}, + }; + + const validations = Object.entries(logic.validations ?? {}); + const computedValues = Object.entries(logic.computedValues ?? {}); + + validations.forEach(([id, validation]) => { + validationMap.set(id, validation); + }); + + computedValues.forEach(([id, computedValue]) => { + computedValuesMap.set(id, computedValue); }); return { - ruleMap, + validationMap, + computedValuesMap, evaluateRule(id, values) { - const validation = ruleMap.get(id); + const validation = validationMap.get(id); const answer = jsonLogic.apply(validation.rule, clean(values)); return answer; }, @@ -30,7 +43,8 @@ function clean(values = {}) { }, {}); } -export function yupSchemaWithCustomJSONLogic(field, validation, id) { +export function yupSchemaWithCustomJSONLogic({ field, validations, id }) { + const validation = validations.validationMap.get(id); return (yupSchema) => yupSchema.test( `${field.name}-validation-${id}`, @@ -48,9 +62,8 @@ function replaceHandlebarsTemplates(string, validations, formValues) { } export function calculateComputedAttributes(fieldParams) { - return ({ validations, field, formValues }) => { + return ({ validations, formValues }) => { const { computedAttributes } = fieldParams; - console.log(field); return Object.fromEntries( Object.entries(computedAttributes) .map(([key, value]) => { diff --git a/src/tests/jsonLogic.test.js b/src/tests/jsonLogic.test.js index 8a4aa7697..272744326 100644 --- a/src/tests/jsonLogic.test.js +++ b/src/tests/jsonLogic.test.js @@ -5,13 +5,14 @@ function createSchemaWithRulesOnFieldA(rules) { properties: { field_a: { type: 'number', - 'x-jsf-validations': rules, + 'x-jsf-requiredValidations': Object.keys(rules), }, field_b: { type: 'number', }, }, required: ['field_a', 'field_b'], + 'x-jsf-logic': { validations: rules }, }; } @@ -20,7 +21,7 @@ function createSchemaWithThreePropertiesWithRuleOnFieldA(rules) { properties: { field_a: { type: 'number', - 'x-jsf-validations': rules, + 'x-jsf-requiredValidations': Object.keys(rules), }, field_b: { type: 'number', @@ -29,6 +30,7 @@ function createSchemaWithThreePropertiesWithRuleOnFieldA(rules) { type: 'number', }, }, + 'x-jsf-logic': { validations: rules }, required: ['field_a', 'field_b', 'field_c'], }; } @@ -178,7 +180,7 @@ describe('cross-value validations', () => { }); }); - describe('Conditionals', () => { + describe.skip('Conditionals', () => { it('when field_a > field_b, show field_c', () => { const schema = { properties: { @@ -379,26 +381,29 @@ describe('cross-value validations', () => { properties: { field_a: { type: 'number', - 'x-jsf-validations': { - a_bigger_than_b: { - errorMessage: 'A must be bigger than B', - rule: { - '>': [{ var: 'field_a' }, { var: 'field_b' }], - }, - }, - is_even_number: { - errorMessage: 'A must be even', - rule: { - '===': [{ '%': [{ var: 'field_a' }, 2] }, 0], - }, - }, - }, + 'x-jsf-requiredValidations': ['a_bigger_than_b', 'is_even_number'], }, field_b: { type: 'number', }, }, required: ['field_a', 'field_b'], + 'x-jsf-logic': { + validations: { + a_bigger_than_b: { + errorMessage: 'A must be bigger than B', + rule: { + '>': [{ var: 'field_a' }, { var: 'field_b' }], + }, + }, + is_even_number: { + errorMessage: 'A must be even', + rule: { + '===': [{ '%': [{ var: 'field_a' }, 2] }, 0], + }, + }, + }, + }, }; const { handleValidation } = createHeadlessForm(schema, { strictInputType: false }); @@ -420,28 +425,30 @@ describe('cross-value validations', () => { properties: { field_a: { type: 'number', - 'x-jsf-validations': { - a_bigger_than_b: { - errorMessage: 'A must be bigger than B', - rule: { - '>': [{ var: 'field_a' }, { var: 'field_b' }], - }, - }, - }, + 'x-jsf-requiredValidations': ['a_bigger_than_b'], }, field_b: { type: 'number', - 'x-jsf-validations': { - is_even_number: { - errorMessage: 'B must be even', - rule: { - '===': [{ '%': [{ var: 'field_b' }, 2] }, 0], - }, + 'x-jsf-requiredValidations': ['is_even_number'], + }, + }, + required: ['field_a', 'field_b'], + 'x-jsf-logic': { + validations: { + a_bigger_than_b: { + errorMessage: 'A must be bigger than B', + rule: { + '>': [{ var: 'field_a' }, { var: 'field_b' }], + }, + }, + is_even_number: { + errorMessage: 'B must be even', + rule: { + '===': [{ '%': [{ var: 'field_b' }, 2] }, 0], }, }, }, }, - required: ['field_a', 'field_b'], }; const { handleValidation } = createHeadlessForm(schema, { strictInputType: false }); @@ -463,16 +470,20 @@ describe('cross-value validations', () => { field_b: { type: 'number', 'x-jsf-computedAttributes': { + title: 'This is {{a_times_two}}!', + value: 'a_times_two', description: 'This field is 2 times bigger than field_a with value of {{a_times_two}}.', }, }, }, required: ['field_a', 'field_b'], - 'x-jsf-validations': { - a_times_two: { - rule: { - '*': [{ var: 'field_a' }, 2], + 'x-jsf-logic': { + validations: { + a_times_two: { + rule: { + '*': [{ var: 'field_a' }, 2], + }, }, }, }, @@ -481,9 +492,12 @@ describe('cross-value validations', () => { strictInputType: false, initialValues: { field_a: 2 }, }); - expect(fields.find((i) => i.name === 'field_b').description).toEqual( + const fieldB = fields.find((i) => i.name === 'field_b'); + expect(fieldB.description).toEqual( 'This field is 2 times bigger than field_a with value of 4.' ); + expect(fieldB.value).toEqual(4); + expect(fieldB.label).toEqual('This is 4!'); }); }); diff --git a/src/yupSchema.js b/src/yupSchema.js index d21b6be7f..0ab00858a 100644 --- a/src/yupSchema.js +++ b/src/yupSchema.js @@ -143,7 +143,7 @@ const getYupSchema = ({ inputType, ...field }) => { * @param {FieldParameters} field Input fields * @returns {Function} Yup schema */ -export function buildYupSchema(field, config) { +export function buildYupSchema(field, config, validations) { const { inputType, jsonType: jsonTypeValue, errorMessage = {}, ...propertyFields } = field; const isCheckboxBoolean = typeof propertyFields.checkboxValue === 'boolean'; let baseSchema; @@ -272,7 +272,8 @@ export function buildYupSchema(field, config) { ...fieldSetfield, inputType: fieldSetfield.type, }, - config + config, + validations )(); } }); @@ -284,7 +285,7 @@ export function buildYupSchema(field, config) { propertyFields.nthFieldGroup.fields().reduce( (schema, groupArrayField) => ({ ...schema, - [groupArrayField.name]: buildYupSchema(groupArrayField, config)(), + [groupArrayField.name]: buildYupSchema(groupArrayField, config, validations)(), }), {} ) @@ -336,9 +337,9 @@ export function buildYupSchema(field, config) { validators.push(withFileFormat); } - if (propertyFields.validations) { - Object.entries(propertyFields.validations).forEach(([id, validation]) => - validators.push(yupSchemaWithCustomJSONLogic(field, validation, id)) + if (propertyFields.requiredValidations) { + propertyFields.requiredValidations.forEach((id) => + validators.push(yupSchemaWithCustomJSONLogic({ field, id, validations })) ); } @@ -358,7 +359,7 @@ export function getNoSortEdges(fields = []) { }, []); } -function getSchema(fields = [], config) { +function getSchema(fields = [], config, validations) { const newSchema = {}; fields.forEach((field) => { @@ -367,13 +368,13 @@ function getSchema(fields = [], config) { if (field.inputType === supportedTypes.FIELDSET) { // Fieldset validation schemas depend on the inner schemas of their fields, // so we need to rebuild it to take into account any of those updates. - const fieldsetSchema = buildYupSchema(field, config)(); + const fieldsetSchema = buildYupSchema(field, config, validations)(); newSchema[field.name] = fieldsetSchema; } else { newSchema[field.name] = field.schema; } } else { - Object.assign(newSchema, getSchema(field.fields, config)); + Object.assign(newSchema, getSchema(field.fields, config, validations)); } } }); @@ -389,6 +390,6 @@ function getSchema(fields = [], config) { * @param {JsfConfig} config - Config * @returns */ -export function buildCompleteYupSchema(fields, config) { - return object().shape(getSchema(fields, config), getNoSortEdges(fields)); +export function buildCompleteYupSchema(fields, config, validations) { + return object().shape(getSchema(fields, config, validations), getNoSortEdges(fields)); } From 1981ba553cedffd0e5b62726a22d10bc778b1dd0 Mon Sep 17 00:00:00 2001 From: brennj Date: Thu, 6 Jul 2023 15:09:36 +0200 Subject: [PATCH 27/69] chore: BOOM IT WORKS --- src/createHeadlessForm.js | 4 + src/helpers.js | 99 +++++-------------- src/jsonLogic.js | 70 ++++++++++++- src/nodeProcessing/checkIfConditionMatches.js | 81 +++++++++++++++ src/tests/jsonLogic.test.js | 56 ++++++----- 5 files changed, 205 insertions(+), 105 deletions(-) create mode 100644 src/nodeProcessing/checkIfConditionMatches.js diff --git a/src/createHeadlessForm.js b/src/createHeadlessForm.js index d8ebda1c8..d316d10a7 100644 --- a/src/createHeadlessForm.js +++ b/src/createHeadlessForm.js @@ -188,6 +188,10 @@ function applyFieldsDependencies(fieldsParameters, node) { applyFieldsDependencies(fieldsParameters, condition); }); } + + if (node?.['x-jsf-logic']) { + applyFieldsDependencies(fieldsParameters, node['x-jsf-logic']); + } } /** diff --git a/src/helpers.js b/src/helpers.js index bda38b1d1..599ca67e5 100644 --- a/src/helpers.js +++ b/src/helpers.js @@ -7,6 +7,8 @@ import { lazy } from 'yup'; import { supportedTypes, getInputType } from './internals/fields'; import { pickXKey } from './internals/helpers'; +import { processJSONLogicNode } from './jsonLogic'; +import { checkIfConditionMatches } from './nodeProcessing/checkIfConditionMatches'; import { containsHTML, hasProperty, wrapWithSpan } from './utils'; import { buildCompleteYupSchema, buildYupSchema } from './yupSchema'; @@ -29,7 +31,7 @@ function hasType(type, typeName) { * @param {Object[]} fields - form fields * @returns */ -function getField(fieldName, fields) { +export function getField(fieldName, fields) { return fields.find(({ name }) => name === fieldName); } @@ -39,7 +41,7 @@ function getField(fieldName, fields) { * @param {any} value * @returns */ -function validateFieldSchema(field, value, validations) { +export function validateFieldSchema(field, value, validations) { const validator = buildYupSchema(field, undefined, validations); return validator().isValidSync(value); } @@ -52,7 +54,7 @@ function validateFieldSchema(field, value, validations) { * @param {any} schemaValue - value specified in the schema * @returns {Boolean} */ -function compareFormValueWithSchemaValue(formValue, schemaValue) { +export function compareFormValueWithSchemaValue(formValue, schemaValue) { // If the value is a number, we can use it directly, otherwise we need to // fallback to undefined since JSON-schemas empty values come represented as null const currentPropertyValue = @@ -62,77 +64,6 @@ function compareFormValueWithSchemaValue(formValue, schemaValue) { return String(formValue) === String(currentPropertyValue); } -/** - * Checks if a "IF" condition matches given the current form state - * @param {Object} node - JSON schema node - * @param {Object} formValues - form state - * @returns {Boolean} - */ -function checkIfConditionMatches(node, formValues, formFields, validations) { - const propertiesMatch = Object.keys(node.if.properties ?? {}).every((name) => { - const currentProperty = node.if.properties[name]; - const value = formValues[name]; - const hasEmptyValue = - typeof value === 'undefined' || - // NOTE: This is a "Remote API" dependency, as empty fields are sent as "null". - value === null; - const hasIfExplicit = node.if.required?.includes(name); - - if (hasEmptyValue && !hasIfExplicit) { - // A property with empty value in a "if" will always match (lead to "then"), - // even if the actual conditional isn't true. Unless it's explicit in the if.required. - // WRONG:: if: { properties: { foo: {...} } } - // CORRECT:: if: { properties: { foo: {...} }, required: ['foo'] } - // Check MR !14408 for further explanation about the official specs - // https://json-schema.org/understanding-json-schema/reference/conditionals.html#if-then-else - return true; - } - - if (hasProperty(currentProperty, 'const')) { - return compareFormValueWithSchemaValue(value, currentProperty.const); - } - - if (currentProperty.contains?.pattern) { - // TODO: remove this || after D#4098 is merged and transformValue does not run for the parser anymore - const formValue = value || []; - - // Making sure the form value type matches the expected type (array) when theres' a "contains" condition - if (Array.isArray(formValue)) { - const pattern = new RegExp(currentProperty.contains.pattern); - return (value || []).some((item) => pattern.test(item)); - } - } - - if (currentProperty.enum) { - return currentProperty.enum.includes(value); - } - - const field = getField(name, formFields); - - return validateFieldSchema( - { - options: field.options, - // @TODO/CODE SMELL. We are passing the property (raw field), but buildYupSchema() expected the output field. - ...currentProperty, - inputType: field.inputType, - required: true, - }, - value, - validations - ); - }); - - const validationsMatch = Object.entries(node.if['x-jsf-validations'] ?? {}).every( - ([name, property]) => { - const currentValue = validations.evaluateRule(name, formValues); - if (Object.hasOwn(property, 'const') && currentValue === property.const) return true; - return false; - } - ); - - return propertiesMatch && validationsMatch; -} - /** * Checks if the provided field has a value (array with positive length or truthy value) * @@ -327,7 +258,13 @@ function updateField(field, requiredFields, node, formValues, validations) { * @param {Set} accRequired - set of required field names gathered by traversing the tree * @returns {Object} */ -function processNode({ node, formValues, formFields, accRequired = new Set(), validations }) { +export function processNode({ + node, + formValues, + formFields, + accRequired = new Set(), + validations, +}) { // Set initial required fields const requiredFields = new Set(accRequired); @@ -414,6 +351,18 @@ function processNode({ node, formValues, formFields, accRequired = new Set(), va }); } + if (node['x-jsf-logic']) { + const { required: requiredFromLogic } = processJSONLogicNode({ + node: node['x-jsf-logic'], + parentNode: node, + formValues, + formFields, + accRequired: requiredFields, + validations, + }); + requiredFromLogic.forEach((field) => requiredFields.add(field)); + } + return { required: requiredFields, }; diff --git a/src/jsonLogic.js b/src/jsonLogic.js index 77b979beb..45b2a292b 100644 --- a/src/jsonLogic.js +++ b/src/jsonLogic.js @@ -1,5 +1,11 @@ import jsonLogic from 'json-logic-js'; +import { processNode } from './helpers'; +import { + checkIfConditionMatches, + checkIfMatchesValidationsAndComputedValues, +} from './nodeProcessing/checkIfConditionMatches'; + /** * Parses the JSON schema to extract the advanced validation logic and returns a set of functionality to check the current status of said rules. * @param {Object} schema - JSON schema node @@ -29,11 +35,16 @@ export function getValidationsFromJSONSchema(schema) { return { validationMap, computedValuesMap, - evaluateRule(id, values) { + evaluateValidationRule(id, values) { const validation = validationMap.get(id); const answer = jsonLogic.apply(validation.rule, clean(values)); return answer; }, + evaluateComputedValueRule(id, values) { + const validation = computedValues.get(id); + const answer = jsonLogic.apply(validation.rule, clean(values)); + return answer; + }, }; } @@ -57,7 +68,7 @@ export function yupSchemaWithCustomJSONLogic({ field, validations, id }) { function replaceHandlebarsTemplates(string, validations, formValues) { return string.replace(/\{\{([^{}]+)\}\}/g, (match, key) => { - return validations.evaluateRule(key.trim(), formValues); + return validations.evaluateValidationRule(key.trim(), formValues); }); } @@ -73,10 +84,63 @@ export function calculateComputedAttributes(fieldParams) { return ['label', replaceHandlebarsTemplates(value, validations, formValues)]; } if (key === 'const' || key === 'value') - return [key, validations.evaluateRule(value, formValues)]; + return [key, validations.evaluateValidationRule(value, formValues)]; return [key, null]; }) .filter(([, value]) => value !== null) ); }; } + +export function processJSONLogicNode({ node, formFields, formValues, accRequired, validations }) { + const requiredFields = new Set(accRequired); + + if (node.allOf) { + node.allOf + .map((allOfNode) => + processJSONLogicNode({ node: allOfNode, formValues, formFields, validations }) + ) + .forEach(({ required: allOfItemRequired }) => { + allOfItemRequired.forEach(requiredFields.add, requiredFields); + }); + } + + if (node.if) { + const matchesPropertyCondition = checkIfConditionMatches( + node, + formValues, + formFields, + validations + ); + const matchesValidationsAndComputedValues = checkIfMatchesValidationsAndComputedValues( + node, + formValues, + validations + ); + + const isConditionMatch = matchesPropertyCondition && matchesValidationsAndComputedValues; + + if (isConditionMatch && node.then) { + const { required: branchRequired } = processNode({ + node: node.then, + formValues, + formFields, + accRequired, + validations, + }); + branchRequired.forEach((field) => requiredFields.add(field)); + } + if (!isConditionMatch && node.else) { + const { required: branchRequired } = processNode({ + node: node.else, + formValues, + formFields, + accRequired: requiredFields, + validations, + }); + branchRequired.forEach((field) => requiredFields.add(field)); + } + } + + return { required: requiredFields }; +} diff --git a/src/nodeProcessing/checkIfConditionMatches.js b/src/nodeProcessing/checkIfConditionMatches.js new file mode 100644 index 000000000..acb73fa59 --- /dev/null +++ b/src/nodeProcessing/checkIfConditionMatches.js @@ -0,0 +1,81 @@ +import { compareFormValueWithSchemaValue, getField, validateFieldSchema } from '../helpers'; +import { hasProperty } from '../utils'; + +/** + * Checks if a "IF" condition matches given the current form state + * @param {Object} node - JSON schema node + * @param {Object} formValues - form state + * @returns {Boolean} + */ +export function checkIfConditionMatches(node, formValues, formFields, validations) { + return Object.keys(node.if.properties ?? {}).every((name) => { + const currentProperty = node.if.properties[name]; + const value = formValues[name]; + const hasEmptyValue = + typeof value === 'undefined' || + // NOTE: This is a "Remote API" dependency, as empty fields are sent as "null". + value === null; + const hasIfExplicit = node.if.required?.includes(name); + + if (hasEmptyValue && !hasIfExplicit) { + // A property with empty value in a "if" will always match (lead to "then"), + // even if the actual conditional isn't true. Unless it's explicit in the if.required. + // WRONG:: if: { properties: { foo: {...} } } + // CORRECT:: if: { properties: { foo: {...} }, required: ['foo'] } + // Check MR !14408 for further explanation about the official specs + // https://json-schema.org/understanding-json-schema/reference/conditionals.html#if-then-else + return true; + } + + if (hasProperty(currentProperty, 'const')) { + return compareFormValueWithSchemaValue(value, currentProperty.const); + } + + if (currentProperty.contains?.pattern) { + // TODO: remove this || after D#4098 is merged and transformValue does not run for the parser anymore + const formValue = value || []; + + // Making sure the form value type matches the expected type (array) when theres' a "contains" condition + if (Array.isArray(formValue)) { + const pattern = new RegExp(currentProperty.contains.pattern); + return (value || []).some((item) => pattern.test(item)); + } + } + + if (currentProperty.enum) { + return currentProperty.enum.includes(value); + } + + const field = getField(name, formFields); + + return validateFieldSchema( + { + options: field.options, + // @TODO/CODE SMELL. We are passing the property (raw field), but buildYupSchema() expected the output field. + ...currentProperty, + inputType: field.inputType, + required: true, + }, + value, + validations + ); + }); +} + +export function checkIfMatchesValidationsAndComputedValues(node, formValues, validations) { + const validationsMatch = Object.entries(node.if.validations ?? {}).every(([name, property]) => { + const currentValue = validations.evaluateValidationRule(name, formValues); + if (Object.hasOwn(property, 'const') && currentValue === property.const) return true; + return false; + }); + + const computedValuesMatch = Object.entries(node.if.computedValues ?? {}).every( + ([name, property]) => { + const currentValue = validations.evaluateComputedValueRule(name, formValues); + if (Object.hasOwn(property, 'const') && currentValue === property.const) return true; + return false; + } + ); + + return computedValuesMatch && validationsMatch; +} diff --git a/src/tests/jsonLogic.test.js b/src/tests/jsonLogic.test.js index 272744326..ff36a4781 100644 --- a/src/tests/jsonLogic.test.js +++ b/src/tests/jsonLogic.test.js @@ -180,7 +180,7 @@ describe('cross-value validations', () => { }); }); - describe.skip('Conditionals', () => { + describe('Conditionals', () => { it('when field_a > field_b, show field_c', () => { const schema = { properties: { @@ -195,35 +195,37 @@ describe('cross-value validations', () => { }, }, required: ['field_a', 'field_b'], - allOf: [ - { - if: { - 'x-jsf-validations': { - require_c: { - const: true, - }, - }, - }, - then: { - required: ['field_c'], - }, - else: { - properties: { - field_c: false, + 'x-jsf-logic': { + validations: { + require_c: { + rule: { + and: [ + { '!==': [{ var: 'field_b' }, null] }, + { '!==': [{ var: 'field_a' }, null] }, + { '>': [{ var: 'field_a' }, { var: 'field_b' }] }, + ], }, }, }, - ], - 'x-jsf-validations': { - require_c: { - rule: { - and: [ - { '!==': [{ var: 'field_b' }, null] }, - { '!==': [{ var: 'field_a' }, null] }, - { '>': [{ var: 'field_a' }, { var: 'field_b' }] }, - ], + allOf: [ + { + if: { + validations: { + require_c: { + const: true, + }, + }, + }, + then: { + required: ['field_c'], + }, + else: { + properties: { + field_c: false, + }, + }, }, - }, + ], }, }; @@ -300,7 +302,7 @@ describe('cross-value validations', () => { expect(handleValidation({ field_a: 5, field_b: 3 }).formErrors).toEqual(undefined); }); - it('Conditionally apply a validation on a property depending on values', () => { + it.skip('Conditionally apply a validation on a property depending on values', () => { const schema = { properties: { field_a: { From 4a7469bf8783fc8ec2f90cd63c83d3fcd1fb85c7 Mon Sep 17 00:00:00 2001 From: brennj Date: Thu, 6 Jul 2023 15:44:49 +0200 Subject: [PATCH 28/69] chore: everything back working! --- src/calculateConditionalProperties.js | 20 ++-- src/createHeadlessForm.js | 3 +- src/helpers.js | 2 +- src/jsonLogic.js | 2 +- src/tests/jsonLogic.test.js | 132 +++++++++++++++++++------- 5 files changed, 112 insertions(+), 47 deletions(-) diff --git a/src/calculateConditionalProperties.js b/src/calculateConditionalProperties.js index 385ca3484..ea83a3f10 100644 --- a/src/calculateConditionalProperties.js +++ b/src/calculateConditionalProperties.js @@ -69,7 +69,7 @@ function rebuildFieldset(fields, property) { * @param {FieldParameters} fieldParams - field parameters * @returns {Function} */ -export function calculateConditionalProperties(fieldParams, customProperties) { +export function calculateConditionalProperties(fieldParams, customProperties, validations) { /** * Runs dynamic property calculation on a field based on a conditional that has been calculated * @param {Boolean} isRequired - if the field is required @@ -102,13 +102,17 @@ export function calculateConditionalProperties(fieldParams, customProperties) { isVisible: true, required: isRequired, ...(presentation?.inputType && { type: presentation.inputType }), - schema: buildYupSchema({ - ...fieldParams, - ...newFieldParams, - // If there are inner fields (case of fieldset) they need to be updated based on the condition - fields: fieldSetFields, - required: isRequired, - }), + schema: buildYupSchema( + { + ...fieldParams, + ...newFieldParams, + // If there are inner fields (case of fieldset) they need to be updated based on the condition + fields: fieldSetFields, + required: isRequired, + }, + undefined, + validations + ), }; return omit(merge(base, presentation, newFieldParams), ['inputType']); diff --git a/src/createHeadlessForm.js b/src/createHeadlessForm.js index d316d10a7..8c5491601 100644 --- a/src/createHeadlessForm.js +++ b/src/createHeadlessForm.js @@ -233,7 +233,8 @@ function buildField(fieldParams, config, scopedJsonSchema, validations) { const yupSchema = buildYupSchema(fieldParams, config, validations); const calculateConditionalFieldsClosure = - fieldParams.isDynamic && calculateConditionalProperties(fieldParams, customProperties); + fieldParams.isDynamic && + calculateConditionalProperties(fieldParams, customProperties, validations); const calculateCustomValidationPropertiesClosure = calculateCustomValidationProperties( fieldParams, diff --git a/src/helpers.js b/src/helpers.js index 599ca67e5..31be18a6d 100644 --- a/src/helpers.js +++ b/src/helpers.js @@ -219,7 +219,7 @@ function updateField(field, requiredFields, node, formValues, validations) { // If field has a calculateConditionalProperties closure, run it and update the field properties if (field.calculateConditionalProperties) { - const newFieldValues = field.calculateConditionalProperties(fieldIsRequired, node); + const newFieldValues = field.calculateConditionalProperties(fieldIsRequired, node, validations); updateValues(newFieldValues); } diff --git a/src/jsonLogic.js b/src/jsonLogic.js index 45b2a292b..c026d61e5 100644 --- a/src/jsonLogic.js +++ b/src/jsonLogic.js @@ -41,7 +41,7 @@ export function getValidationsFromJSONSchema(schema) { return answer; }, evaluateComputedValueRule(id, values) { - const validation = computedValues.get(id); + const validation = computedValuesMap.get(id); const answer = jsonLogic.apply(validation.rule, clean(values)); return answer; }, diff --git a/src/tests/jsonLogic.test.js b/src/tests/jsonLogic.test.js index ff36a4781..b05e22d76 100644 --- a/src/tests/jsonLogic.test.js +++ b/src/tests/jsonLogic.test.js @@ -302,7 +302,7 @@ describe('cross-value validations', () => { expect(handleValidation({ field_a: 5, field_b: 3 }).formErrors).toEqual(undefined); }); - it.skip('Conditionally apply a validation on a property depending on values', () => { + it('Conditionally apply a validation on a property depending on values', () => { const schema = { properties: { field_a: { @@ -316,48 +316,49 @@ describe('cross-value validations', () => { }, }, required: ['field_a', 'field_b'], - allOf: [ - { - if: { - 'x-jsf-validations': { - require_c: { - const: true, - }, + 'x-jsf-logic': { + validations: { + c_must_be_large: { + errorMessage: 'Needs more numbers', + rule: { + '>': [{ var: 'field_c' }, 200], }, }, - then: { - required: ['field_c'], - properties: { - field_c: { - description: 'I am a description!', - 'x-jsf-validations': { - c_must_be_large: { - errorMessage: 'Needs more numbers', - rule: { - '>': [{ var: 'field_c' }, 200], - }, - }, + require_c: { + rule: { + and: [ + { '!==': [{ var: 'field_b' }, null] }, + { '!==': [{ var: 'field_a' }, null] }, + { '>': [{ var: 'field_a' }, { var: 'field_b' }] }, + ], + }, + }, + }, + allOf: [ + { + if: { + validations: { + require_c: { + const: true, }, }, }, - }, - else: { - properties: { - field_c: false, + then: { + required: ['field_c'], + properties: { + field_c: { + description: 'I am a description!', + 'x-jsf-requiredValidations': ['c_must_be_large'], + }, + }, + }, + else: { + properties: { + field_c: false, + }, }, }, - }, - ], - 'x-jsf-validations': { - require_c: { - rule: { - and: [ - { '!==': [{ var: 'field_b' }, null] }, - { '!==': [{ var: 'field_a' }, null] }, - { '>': [{ var: 'field_a' }, { var: 'field_b' }] }, - ], - }, - }, + ], }, }; const { fields, handleValidation } = createHeadlessForm(schema, { strictInputType: false }); @@ -375,6 +376,65 @@ describe('cross-value validations', () => { undefined ); }); + + it('Should apply a conditional based on a true computedValue', () => { + const schema = { + properties: { + field_a: { + type: 'number', + }, + field_b: { + type: 'number', + }, + field_c: { + type: 'number', + }, + }, + required: ['field_a', 'field_b'], + 'x-jsf-logic': { + computedValues: { + require_c: { + rule: { + and: [ + { '!==': [{ var: 'field_b' }, null] }, + { '!==': [{ var: 'field_a' }, null] }, + { '>': [{ var: 'field_a' }, { var: 'field_b' }] }, + ], + }, + }, + }, + allOf: [ + { + if: { + computedValues: { + require_c: { + const: true, + }, + }, + }, + then: { + required: ['field_c'], + }, + else: { + properties: { + field_c: false, + }, + }, + }, + ], + }, + }; + const { fields, handleValidation } = createHeadlessForm(schema, { strictInputType: false }); + const cField = fields.find((i) => i.name === 'field_c'); + expect(cField.isVisible).toEqual(false); + expect(cField.description).toEqual(undefined); + expect(handleValidation({ field_a: 10, field_b: 5 }).formErrors).toEqual({ + field_c: 'Required field', + }); + expect(handleValidation({ field_a: 10, field_b: 5, field_c: 201 }).formErrors).toEqual( + undefined + ); + }); }); describe('Multiple validations', () => { From a28121a9a5581aa3fabc1981658361be848a6307 Mon Sep 17 00:00:00 2001 From: brennj Date: Thu, 6 Jul 2023 15:55:51 +0200 Subject: [PATCH 29/69] chore: current progress and writing more tests --- src/jsonLogic.js | 4 +- src/tests/jsonLogic.test.js | 82 ++++++++++++++++++++++++++++++++++++- 2 files changed, 83 insertions(+), 3 deletions(-) diff --git a/src/jsonLogic.js b/src/jsonLogic.js index c026d61e5..eb1ceb203 100644 --- a/src/jsonLogic.js +++ b/src/jsonLogic.js @@ -68,7 +68,7 @@ export function yupSchemaWithCustomJSONLogic({ field, validations, id }) { function replaceHandlebarsTemplates(string, validations, formValues) { return string.replace(/\{\{([^{}]+)\}\}/g, (match, key) => { - return validations.evaluateValidationRule(key.trim(), formValues); + return validations.evaluateComputedValueRule(key.trim(), formValues); }); } @@ -84,7 +84,7 @@ export function calculateComputedAttributes(fieldParams) { return ['label', replaceHandlebarsTemplates(value, validations, formValues)]; } if (key === 'const' || key === 'value') - return [key, validations.evaluateValidationRule(value, formValues)]; + return [key, validations.evaluateComputedValueRule(value, formValues)]; return [key, null]; }) .filter(([, value]) => value !== null) diff --git a/src/tests/jsonLogic.test.js b/src/tests/jsonLogic.test.js index b05e22d76..8b68ec1fc 100644 --- a/src/tests/jsonLogic.test.js +++ b/src/tests/jsonLogic.test.js @@ -520,6 +520,86 @@ describe('cross-value validations', () => { }); expect(handleValidation({ field_a: 4, field_b: 2 }).formErrors).toEqual(undefined); }); + + it.todo('Need to handle checking a const that is a number in an if for a computed value.'); + + it.skip('Advanced example driving multiple concepts together', () => { + const schema = { + properties: { + field_a: { + type: 'number', + }, + field_b: { + type: 'number', + 'x-jsf-requiredValidations': ['divisible_by_five'], + 'x-jsf-computedAttributes': { + title: 'This is {{a_times_two}}!', + value: 'a_times_two', + description: + 'This field is 2 times bigger than field_a with value of {{a_times_two}}.', + }, + }, + field_c: { + type: 'number', + }, + }, + required: ['field_a', 'field_b'], + 'x-jsf-logic': { + validations: { + divisible_by_five: { + errorMessage: 'This value must be divisible by 5!', + rule: { + '===': [{ '%': [{ var: 'field_b' }, 5] }, 0], + }, + }, + double_b: { + errorMessage: 'Must be two times A', + rule: { + '>': [{ var: 'field_c' }, { '*': [{ var: 'field_b' }, 2] }], + }, + }, + }, + computedValues: { + a_times_two: { + rule: { + '*': [{ var: 'field_a' }, 2], + }, + }, + }, + allOf: [ + { + if: { + computedValues: { + a_times_two: { + const: 10, + }, + }, + validations: { + divisible_by_five: { + const: false, + }, + }, + }, + then: { + required: ['field_c'], + properties: { + field_c: { + 'x-jsf-requiredValidations': ['double_b'], + title: 'Adding a title.', + }, + }, + }, + else: { + properties: { + field_c: false, + }, + }, + }, + ], + }, + }; + createHeadlessForm(schema, { strictInputType: false }); + }); }); describe('Derive values', () => { @@ -541,7 +621,7 @@ describe('cross-value validations', () => { }, required: ['field_a', 'field_b'], 'x-jsf-logic': { - validations: { + computedValues: { a_times_two: { rule: { '*': [{ var: 'field_a' }, 2], From ebb3dd39fbfa610d2c9cf7166279daffe15ec6e1 Mon Sep 17 00:00:00 2001 From: brennj Date: Fri, 7 Jul 2023 10:33:54 +0200 Subject: [PATCH 30/69] chore: validations + computedValues anded together --- src/tests/jsonLogic.test.js | 225 +++++++++++++++++++++++------------- 1 file changed, 147 insertions(+), 78 deletions(-) diff --git a/src/tests/jsonLogic.test.js b/src/tests/jsonLogic.test.js index 8b68ec1fc..bac8d1590 100644 --- a/src/tests/jsonLogic.test.js +++ b/src/tests/jsonLogic.test.js @@ -435,6 +435,153 @@ describe('cross-value validations', () => { undefined ); }); + + it('Handle multiple computedValue checks by ANDing them together', () => { + const schema = { + properties: { + field_a: { + type: 'number', + }, + field_b: { + type: 'number', + }, + field_c: { + type: 'number', + }, + }, + required: ['field_a', 'field_b'], + 'x-jsf-logic': { + validations: { + double_b: { + errorMessage: 'Must be two times B', + rule: { + '>': [{ var: 'field_c' }, { '*': [{ var: 'field_b' }, 2] }], + }, + }, + }, + computedValues: { + a_times_two: { + rule: { + '*': [{ var: 'field_a' }, 2], + }, + }, + mod_by_five: { + rule: { + '%': [{ var: 'field_b' }, 5], + }, + }, + }, + allOf: [ + { + if: { + computedValues: { + a_times_two: { + const: 20, + }, + mod_by_five: { + const: 3, + }, + }, + }, + then: { + required: ['field_c'], + properties: { + field_c: { + 'x-jsf-requiredValidations': ['double_b'], + title: 'Adding a title.', + }, + }, + }, + else: { + properties: { + field_c: false, + }, + }, + }, + ], + }, + }; + const { handleValidation } = createHeadlessForm(schema, { strictInputType: false }); + expect(handleValidation({}).formErrors).toEqual({ + field_a: 'Required field', + field_b: 'Required field', + }); + expect(handleValidation({ field_a: 10, field_b: 8 }).formErrors).toEqual({ + field_c: 'Required field', + }); + expect(handleValidation({ field_a: 10, field_b: 8, field_c: 0 }).formErrors).toEqual({ + field_c: 'Must be two times B', + }); + expect(handleValidation({ field_a: 10, field_b: 8, field_c: 17 }).formErrors).toEqual( + undefined + ); + }); + + it('Handle having a true condition with both validations and computedValue checks', () => { + const schema = { + properties: { + field_a: { + type: 'number', + }, + field_b: { + type: 'number', + }, + field_c: { + type: 'number', + }, + }, + required: ['field_a', 'field_b'], + 'x-jsf-logic': { + validations: { + greater_than_b: { + rule: { + '>': [{ var: 'field_a' }, { var: 'field_b' }], + }, + }, + }, + computedValues: { + a_times_two: { + rule: { + '*': [{ var: 'field_a' }, 2], + }, + }, + }, + allOf: [ + { + if: { + computedValues: { + a_times_two: { + const: 20, + }, + }, + validations: { + greater_than_b: { + const: true, + }, + }, + }, + then: { + required: ['field_c'], + }, + else: { + properties: { + field_c: false, + }, + }, + }, + ], + }, + }; + const { handleValidation } = createHeadlessForm(schema, { strictInputType: false }); + expect(handleValidation({ field_a: 1, field_b: 1 }).formErrors).toEqual(undefined); + expect(handleValidation({ field_a: 10, field_b: 20 }).formErrors).toEqual(undefined); + expect(handleValidation({ field_a: 10, field_b: 9 }).formErrors).toEqual({ + field_c: 'Required field', + }); + expect(handleValidation({ field_a: 10, field_b: 9, field_c: 10 }).formErrors).toEqual( + undefined + ); + }); }); describe('Multiple validations', () => { @@ -522,84 +669,6 @@ describe('cross-value validations', () => { }); it.todo('Need to handle checking a const that is a number in an if for a computed value.'); - - it.skip('Advanced example driving multiple concepts together', () => { - const schema = { - properties: { - field_a: { - type: 'number', - }, - field_b: { - type: 'number', - 'x-jsf-requiredValidations': ['divisible_by_five'], - 'x-jsf-computedAttributes': { - title: 'This is {{a_times_two}}!', - value: 'a_times_two', - description: - 'This field is 2 times bigger than field_a with value of {{a_times_two}}.', - }, - }, - field_c: { - type: 'number', - }, - }, - required: ['field_a', 'field_b'], - 'x-jsf-logic': { - validations: { - divisible_by_five: { - errorMessage: 'This value must be divisible by 5!', - rule: { - '===': [{ '%': [{ var: 'field_b' }, 5] }, 0], - }, - }, - double_b: { - errorMessage: 'Must be two times A', - rule: { - '>': [{ var: 'field_c' }, { '*': [{ var: 'field_b' }, 2] }], - }, - }, - }, - computedValues: { - a_times_two: { - rule: { - '*': [{ var: 'field_a' }, 2], - }, - }, - }, - allOf: [ - { - if: { - computedValues: { - a_times_two: { - const: 10, - }, - }, - validations: { - divisible_by_five: { - const: false, - }, - }, - }, - then: { - required: ['field_c'], - properties: { - field_c: { - 'x-jsf-requiredValidations': ['double_b'], - title: 'Adding a title.', - }, - }, - }, - else: { - properties: { - field_c: false, - }, - }, - }, - ], - }, - }; - createHeadlessForm(schema, { strictInputType: false }); - }); }); describe('Derive values', () => { From 9483cf7e1ca9c40089e401569b74d6e65379b7c2 Mon Sep 17 00:00:00 2001 From: brennj Date: Fri, 7 Jul 2023 13:12:13 +0200 Subject: [PATCH 31/69] chore: first of fieldsets working --- src/calculateConditionalProperties.js | 2 +- src/createHeadlessForm.js | 27 ++++++++----- src/jsonLogic.js | 35 ++++++++++++++--- src/nodeProcessing/checkIfConditionMatches.js | 4 +- src/tests/jsonLogic.test.js | 39 +++++++++++++++++-- src/yupSchema.js | 8 +++- 6 files changed, 92 insertions(+), 23 deletions(-) diff --git a/src/calculateConditionalProperties.js b/src/calculateConditionalProperties.js index ea83a3f10..ec83d1f4f 100644 --- a/src/calculateConditionalProperties.js +++ b/src/calculateConditionalProperties.js @@ -110,7 +110,7 @@ export function calculateConditionalProperties(fieldParams, customProperties, va fields: fieldSetFields, required: isRequired, }, - undefined, + { config: { parentID: fieldParams.name } }, validations ), }; diff --git a/src/createHeadlessForm.js b/src/createHeadlessForm.js index 8c5491601..7c6335bda 100644 --- a/src/createHeadlessForm.js +++ b/src/createHeadlessForm.js @@ -24,7 +24,7 @@ import { getInputType, } from './internals/fields'; import { pickXKey } from './internals/helpers'; -import { calculateComputedAttributes, getValidationsFromJSONSchema } from './jsonLogic'; +import { calculateComputedAttributes, createValidationChecker } from './jsonLogic'; import { buildYupSchema } from './yupSchema'; // Some type definitions (to be migrated into .d.ts file or TS Interfaces) @@ -100,7 +100,7 @@ function removeInvalidAttributes(fields) { * * @returns {FieldParameters} */ -function buildFieldParameters(name, fieldProperties, required = [], config = {}) { +function buildFieldParameters(name, fieldProperties, required = [], config = {}, validations) { const { position } = pickXKey(fieldProperties, 'presentation') ?? {}; let fields; @@ -108,9 +108,13 @@ function buildFieldParameters(name, fieldProperties, required = [], config = {}) if (inputType === supportedTypes.FIELDSET) { // eslint-disable-next-line no-use-before-define - fields = getFieldsFromJSONSchema(fieldProperties, { - customProperties: get(config, `customProperties.${name}`, {}), - }); + fields = getFieldsFromJSONSchema( + fieldProperties, + { + customProperties: get(config, `customProperties.${name}`, {}), + }, + validations + ); } const result = { @@ -137,7 +141,8 @@ function buildFieldParameters(name, fieldProperties, required = [], config = {}) */ function convertJSONSchemaPropertiesToFieldParameters( { properties, required, 'x-jsf-order': order }, - config = {} + config = {}, + validations ) { const sortFields = (a, b) => sortByOrderOrPosition(a, b, order); @@ -145,7 +150,7 @@ function convertJSONSchemaPropertiesToFieldParameters( // their position and then remove the position property (since it's no longer needed) return Object.entries(properties) .filter(([, value]) => typeof value === 'object') - .map(([key, value]) => buildFieldParameters(key, value, required, config)) + .map(([key, value]) => buildFieldParameters(key, value, required, config, validations)) .sort(sortFields) .map(({ position, ...fieldParams }) => fieldParams); } @@ -283,7 +288,11 @@ function getFieldsFromJSONSchema(scopedJsonSchema, config, validations) { return []; } - const fieldParamsList = convertJSONSchemaPropertiesToFieldParameters(scopedJsonSchema, config); + const fieldParamsList = convertJSONSchemaPropertiesToFieldParameters( + scopedJsonSchema, + config, + validations + ); applyFieldsDependencies(fieldParamsList, scopedJsonSchema); @@ -333,7 +342,7 @@ export function createHeadlessForm(jsonSchema, customConfig = {}) { }; try { - const validations = getValidationsFromJSONSchema(jsonSchema); + const validations = createValidationChecker(jsonSchema); const fields = getFieldsFromJSONSchema(jsonSchema, config, validations); const handleValidation = handleValuesChange(fields, jsonSchema, config, validations); diff --git a/src/jsonLogic.js b/src/jsonLogic.js index eb1ceb203..426f3dec5 100644 --- a/src/jsonLogic.js +++ b/src/jsonLogic.js @@ -12,7 +12,29 @@ import { * @param {Object} initialValues - form state * @returns {Object} */ -export function getValidationsFromJSONSchema(schema) { +export function createValidationChecker(schema) { + const scopes = new Map(); + + function createScopes(jsonSchema, key = 'root') { + scopes.set(key, createValidationsScope(jsonSchema)); + Object.entries(jsonSchema.properties) + .filter(([, property]) => property.type === 'object') + .forEach(([key, property]) => { + createScopes(property, key); + }); + } + + createScopes(schema); + + return { + scopes, + getScope(name = 'root') { + return scopes.get(name); + }, + }; +} + +function createValidationsScope(schema) { const validationMap = new Map(); const computedValuesMap = new Map(); @@ -54,12 +76,13 @@ function clean(values = {}) { }, {}); } -export function yupSchemaWithCustomJSONLogic({ field, validations, id }) { - const validation = validations.validationMap.get(id); +export function yupSchemaWithCustomJSONLogic({ field, validations, config, id }) { + const { parentID = 'root' } = config; + const validation = validations.getScope(parentID).validationMap.get(id); return (yupSchema) => yupSchema.test( `${field.name}-validation-${id}`, - validation.errorMessage ?? 'This field is invalid.', + validation?.errorMessage ?? 'This field is invalid.', (_, { parent }) => { return jsonLogic.apply(validation.rule, parent); } @@ -68,7 +91,7 @@ export function yupSchemaWithCustomJSONLogic({ field, validations, id }) { function replaceHandlebarsTemplates(string, validations, formValues) { return string.replace(/\{\{([^{}]+)\}\}/g, (match, key) => { - return validations.evaluateComputedValueRule(key.trim(), formValues); + return validations.getScope().evaluateComputedValueRule(key.trim(), formValues); }); } @@ -84,7 +107,7 @@ export function calculateComputedAttributes(fieldParams) { return ['label', replaceHandlebarsTemplates(value, validations, formValues)]; } if (key === 'const' || key === 'value') - return [key, validations.evaluateComputedValueRule(value, formValues)]; + return [key, validations.getScope().evaluateComputedValueRule(value, formValues)]; return [key, null]; }) .filter(([, value]) => value !== null) diff --git a/src/nodeProcessing/checkIfConditionMatches.js b/src/nodeProcessing/checkIfConditionMatches.js index acb73fa59..4336aef85 100644 --- a/src/nodeProcessing/checkIfConditionMatches.js +++ b/src/nodeProcessing/checkIfConditionMatches.js @@ -64,14 +64,14 @@ export function checkIfConditionMatches(node, formValues, formFields, validation export function checkIfMatchesValidationsAndComputedValues(node, formValues, validations) { const validationsMatch = Object.entries(node.if.validations ?? {}).every(([name, property]) => { - const currentValue = validations.evaluateValidationRule(name, formValues); + const currentValue = validations.getScope().evaluateValidationRule(name, formValues); if (Object.hasOwn(property, 'const') && currentValue === property.const) return true; return false; }); const computedValuesMatch = Object.entries(node.if.computedValues ?? {}).every( ([name, property]) => { - const currentValue = validations.evaluateComputedValueRule(name, formValues); + const currentValue = validations.getScope().evaluateComputedValueRule(name, formValues); if (Object.hasOwn(property, 'const') && currentValue === property.const) return true; return false; } diff --git a/src/tests/jsonLogic.test.js b/src/tests/jsonLogic.test.js index bac8d1590..0af6a237c 100644 --- a/src/tests/jsonLogic.test.js +++ b/src/tests/jsonLogic.test.js @@ -667,8 +667,6 @@ describe('cross-value validations', () => { }); expect(handleValidation({ field_a: 4, field_b: 2 }).formErrors).toEqual(undefined); }); - - it.todo('Need to handle checking a const that is a number in an if for a computed value.'); }); describe('Derive values', () => { @@ -713,7 +711,42 @@ describe('cross-value validations', () => { }); describe('Nested fieldsets', () => { - it.todo('Does everything above work when the field is nested'); + it('Does everything above work when the field is nested', () => { + const schema = { + properties: { + field_a: { + type: 'object', + 'x-jsf-presentation': { + inputType: 'fieldset', + }, + properties: { + child: { + type: 'number', + 'x-jsf-requiredValidations': ['child_greater_than_10'], + }, + }, + required: ['child'], + 'x-jsf-logic': { + validations: { + child_greater_than_10: { + errorMessage: 'Must be greater than 10!', + rule: { + '>': [{ var: 'child' }, 10], + }, + }, + }, + }, + }, + }, + required: ['field_a'], + }; + const { handleValidation } = createHeadlessForm(schema, { strictInputType: false }); + expect(handleValidation({}).formErrors).toEqual({ field_a: { child: 'Required field' } }); + expect(handleValidation({ field_a: { child: 0 } }).formErrors).toEqual({ + field_a: { child: 'Must be greater than 10!' }, + }); + }); + it.todo('Validate a field and a nested field together'); it.todo('compute a nested field attribute'); }); diff --git a/src/yupSchema.js b/src/yupSchema.js index 0ab00858a..5397e2b77 100644 --- a/src/yupSchema.js +++ b/src/yupSchema.js @@ -339,7 +339,7 @@ export function buildYupSchema(field, config, validations) { if (propertyFields.requiredValidations) { propertyFields.requiredValidations.forEach((id) => - validators.push(yupSchemaWithCustomJSONLogic({ field, id, validations })) + validators.push(yupSchemaWithCustomJSONLogic({ field, id, validations, config })) ); } @@ -368,7 +368,11 @@ function getSchema(fields = [], config, validations) { if (field.inputType === supportedTypes.FIELDSET) { // Fieldset validation schemas depend on the inner schemas of their fields, // so we need to rebuild it to take into account any of those updates. - const fieldsetSchema = buildYupSchema(field, config, validations)(); + const fieldsetSchema = buildYupSchema( + field, + { ...config, parentID: field.name }, + validations + )(); newSchema[field.name] = fieldsetSchema; } else { newSchema[field.name] = field.schema; From e2a69e564ce7137c06786de34eb11342722996a0 Mon Sep 17 00:00:00 2001 From: brennj Date: Fri, 7 Jul 2023 13:19:25 +0200 Subject: [PATCH 32/69] chore: more tests --- src/tests/jsonLogic.test.js | 58 ++++++++++++++++++++++++++++++++++++- 1 file changed, 57 insertions(+), 1 deletion(-) diff --git a/src/tests/jsonLogic.test.js b/src/tests/jsonLogic.test.js index 0af6a237c..2b6eb392e 100644 --- a/src/tests/jsonLogic.test.js +++ b/src/tests/jsonLogic.test.js @@ -36,6 +36,10 @@ function createSchemaWithThreePropertiesWithRuleOnFieldA(rules) { } describe('cross-value validations', () => { + describe('Does not conflict with native JSON schema', () => { + it.todo('When a field is not required, validations should not block submitting'); + }); + describe('Relative: <, >, =', () => { it('bigger: field_a > field_b', () => { const schema = createSchemaWithRulesOnFieldA({ @@ -711,7 +715,7 @@ describe('cross-value validations', () => { }); describe('Nested fieldsets', () => { - it('Does everything above work when the field is nested', () => { + it('Basic nested validation works', () => { const schema = { properties: { field_a: { @@ -745,6 +749,58 @@ describe('cross-value validations', () => { expect(handleValidation({ field_a: { child: 0 } }).formErrors).toEqual({ field_a: { child: 'Must be greater than 10!' }, }); + expect(handleValidation({ field_a: { child: 11 } }).formErrors).toEqual(undefined); + }); + + it('Validating two nested fields together', () => { + const schema = { + properties: { + field_a: { + type: 'object', + 'x-jsf-presentation': { + inputType: 'fieldset', + }, + properties: { + child: { + type: 'number', + 'x-jsf-requiredValidations': ['child_greater_than_10'], + }, + other_child: { + type: 'number', + 'x-jsf-requiredValidations': ['greater_than_child'], + }, + }, + required: ['child', 'other_child'], + 'x-jsf-logic': { + validations: { + child_greater_than_10: { + errorMessage: 'Must be greater than 10!', + rule: { + '>': [{ var: 'child' }, 10], + }, + }, + greater_than_child: { + errorMessage: 'Must be greater than child', + rule: { + '>': [{ var: 'other_child' }, { var: 'child' }], + }, + }, + }, + }, + }, + }, + required: ['field_a'], + }; + const { handleValidation } = createHeadlessForm(schema, { strictInputType: false }); + expect(handleValidation({}).formErrors).toEqual({ + field_a: { child: 'Required field', other_child: 'Required field' }, + }); + expect(handleValidation({ field_a: { child: 0, other_child: 0 } }).formErrors).toEqual({ + field_a: { child: 'Must be greater than 10!', other_child: 'Must be greater than child' }, + }); + expect(handleValidation({ field_a: { child: 11, other_child: 12 } }).formErrors).toEqual( + undefined + ); }); it.todo('Validate a field and a nested field together'); From 260a557fd31bd0065f8576c18d6bf064ef753c18 Mon Sep 17 00:00:00 2001 From: brennj Date: Fri, 7 Jul 2023 14:32:56 +0200 Subject: [PATCH 33/69] chore: error tests --- src/jsonLogic.js | 14 +++++- src/tests/jsonLogic.test.js | 93 ++++++++++++++++++++++++++++++++++++- 2 files changed, 104 insertions(+), 3 deletions(-) diff --git a/src/jsonLogic.js b/src/jsonLogic.js index 426f3dec5..98b766777 100644 --- a/src/jsonLogic.js +++ b/src/jsonLogic.js @@ -17,7 +17,7 @@ export function createValidationChecker(schema) { function createScopes(jsonSchema, key = 'root') { scopes.set(key, createValidationsScope(jsonSchema)); - Object.entries(jsonSchema.properties) + Object.entries(jsonSchema?.properties ?? {}) .filter(([, property]) => property.type === 'object') .forEach(([key, property]) => { createScopes(property, key); @@ -47,10 +47,18 @@ function createValidationsScope(schema) { const computedValues = Object.entries(logic.computedValues ?? {}); validations.forEach(([id, validation]) => { + if (!validation.rule) { + throw Error(`Missing rule for validation with id of: "${id}".`); + } + validationMap.set(id, validation); }); computedValues.forEach(([id, computedValue]) => { + if (!computedValue.rule) { + throw Error(`Missing rule for computedValue with id of: "${id}".`); + } + computedValuesMap.set(id, computedValue); }); @@ -79,11 +87,13 @@ function clean(values = {}) { export function yupSchemaWithCustomJSONLogic({ field, validations, config, id }) { const { parentID = 'root' } = config; const validation = validations.getScope(parentID).validationMap.get(id); + return (yupSchema) => yupSchema.test( `${field.name}-validation-${id}`, validation?.errorMessage ?? 'This field is invalid.', - (_, { parent }) => { + (value, { parent }) => { + if (value === undefined && !field.required) return true; return jsonLogic.apply(validation.rule, parent); } ); diff --git a/src/tests/jsonLogic.test.js b/src/tests/jsonLogic.test.js index 2b6eb392e..b9e3747d6 100644 --- a/src/tests/jsonLogic.test.js +++ b/src/tests/jsonLogic.test.js @@ -37,7 +37,98 @@ function createSchemaWithThreePropertiesWithRuleOnFieldA(rules) { describe('cross-value validations', () => { describe('Does not conflict with native JSON schema', () => { - it.todo('When a field is not required, validations should not block submitting'); + it('When a field is not required, validations should not block submitting when its an empty value', () => { + const schema = { + properties: { + field_a: { + type: 'number', + 'x-jsf-requiredValidations': ['a_greater_than_ten'], + }, + }, + 'x-jsf-logic': { + validations: { + a_greater_than_ten: { + errorMessage: 'Must be greater than 10', + rule: { + '>': [{ var: 'field_a' }, 10], + }, + }, + }, + }, + required: [], + }; + const { handleValidation } = createHeadlessForm(schema, { strictInputType: false }); + expect(handleValidation({}).formErrors).toEqual(undefined); + expect(handleValidation({ field_a: 0 }).formErrors).toEqual({ + field_a: 'Must be greater than 10', + }); + expect(handleValidation({ field_a: 'incorrect value' }).formErrors).toEqual({ + field_a: 'The value must be a number', + }); + expect(handleValidation({ field_a: 11 }).formErrors).toEqual(undefined); + }); + + it('Native validations always appear first', () => { + const schema = { + properties: { + field_a: { + type: 'number', + minimum: 5, + 'x-jsf-requiredValidations': ['a_greater_than_ten'], + }, + }, + 'x-jsf-logic': { + validations: { + a_greater_than_ten: { + errorMessage: 'Must be greater than 10', + rule: { + '>': [{ var: 'field_a' }, 10], + }, + }, + }, + }, + required: ['field_a'], + }; + const { handleValidation } = createHeadlessForm(schema, { strictInputType: false }); + expect(handleValidation({}).formErrors).toEqual({ field_a: 'Required field' }); + expect(handleValidation({ field_a: 0 }).formErrors).toEqual({ + field_a: 'Must be greater or equal to 5', + }); + expect(handleValidation({ field_a: 5 }).formErrors).toEqual({ + field_a: 'Must be greater than 10', + }); + }); + }); + + describe('Badly written schemas', () => { + it('Should throw when theres a missing rule', () => { + const schema = { + properties: { + field_a: { + type: 'number', + 'x-jsf-requiredValidations': ['a_greater_than_ten'], + }, + }, + 'x-jsf-logic': { + validations: { + a_greater_than_ten: { + errorMessage: 'Must be greater than 10', + }, + }, + }, + required: [], + }; + jest.spyOn(console, 'error').mockImplementation(() => {}); + + createHeadlessForm(schema, { strictInputType: false }); + expect(console.error).toHaveBeenCalledWith( + 'JSON Schema invalid!', + Error('Missing rule for validation with id of: "a_greater_than_ten".') + ); + console.error.mockRestore(); + }); + + it.todo('Should throw when a var does not exist in a rule.'); }); describe('Relative: <, >, =', () => { From 75dc8c73e375fcc14ef5fe4ddb0b27f27ae99bdd Mon Sep 17 00:00:00 2001 From: brennj Date: Fri, 7 Jul 2023 15:00:19 +0200 Subject: [PATCH 34/69] chore: more error handling, throw on top lvl non existing vars --- src/jsonLogic.js | 27 +++++++++++++++++++++++++++ src/tests/jsonLogic.test.js | 34 +++++++++++++++++++++++++++++++++- 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/src/jsonLogic.js b/src/jsonLogic.js index 98b766777..dab4b3f55 100644 --- a/src/jsonLogic.js +++ b/src/jsonLogic.js @@ -45,15 +45,32 @@ function createValidationsScope(schema) { const validations = Object.entries(logic.validations ?? {}); const computedValues = Object.entries(logic.computedValues ?? {}); + const sampleEmptyObject = buildSampleEmptyObject(schema); validations.forEach(([id, validation]) => { if (!validation.rule) { throw Error(`Missing rule for validation with id of: "${id}".`); } + checkDataIntegrity(validation.rule, id, sampleEmptyObject); + validationMap.set(id, validation); }); + function checkDataIntegrity(rule, id) { + Object.values(rule).map((subRule) => { + subRule.map((item) => { + const isVar = Object.hasOwn(item, 'var'); + if (isVar) { + const exists = jsonLogic.apply({ var: item.var }, sampleEmptyObject); + if (exists === null) { + throw Error(`"${item.var}" in rule "${id}" does not exist as a JSON schema property.`); + } + } + }); + }); + } + computedValues.forEach(([id, computedValue]) => { if (!computedValue.rule) { throw Error(`Missing rule for computedValue with id of: "${id}".`); @@ -177,3 +194,13 @@ export function processJSONLogicNode({ node, formFields, formValues, accRequired return { required: requiredFields }; } + +function buildSampleEmptyObject(schema) { + const { properties } = schema; + return Object.fromEntries( + Object.entries(properties ?? {}).map(([key, value]) => { + if (value.type !== 'object') return [key, true]; + return [key, buildSampleEmptyObject(value)]; + }) + ); +} diff --git a/src/tests/jsonLogic.test.js b/src/tests/jsonLogic.test.js index b9e3747d6..d3b0fa72f 100644 --- a/src/tests/jsonLogic.test.js +++ b/src/tests/jsonLogic.test.js @@ -128,7 +128,39 @@ describe('cross-value validations', () => { console.error.mockRestore(); }); - it.todo('Should throw when a var does not exist in a rule.'); + it('Should throw when a var does not exist in a rule.', () => { + const schema = { + properties: { + field_a: { + type: 'number', + }, + }, + 'x-jsf-logic': { + validations: { + a_greater_than_ten: { + errorMessage: 'Must be greater than 10', + rule: { + '>': [{ var: 'field_b' }, 10], + }, + }, + }, + }, + required: [], + }; + jest.spyOn(console, 'error').mockImplementation(() => {}); + + createHeadlessForm(schema, { strictInputType: false }); + expect(console.error).toHaveBeenCalledWith( + 'JSON Schema invalid!', + Error('"field_b" in rule "a_greater_than_ten" does not exist as a JSON schema property.') + ); + console.error.mockRestore(); + }); + + it.todo('Should throw when a var does not exist in a deeply nested rule'); + + it.todo('Should throw when a var does not exist in a fieldset.'); + it.todo('Should throw when a var does not exist in an array.'); }); describe('Relative: <, >, =', () => { From cc42feed643e30094202625091f3902b1c657c78 Mon Sep 17 00:00:00 2001 From: brennj Date: Fri, 7 Jul 2023 15:09:29 +0200 Subject: [PATCH 35/69] chore: extended error checks for deeply nested rules --- src/jsonLogic.js | 30 ++++++++++++++++-------------- src/tests/jsonLogic.test.js | 32 +++++++++++++++++++++++++++++++- 2 files changed, 47 insertions(+), 15 deletions(-) diff --git a/src/jsonLogic.js b/src/jsonLogic.js index dab4b3f55..f6ec03fac 100644 --- a/src/jsonLogic.js +++ b/src/jsonLogic.js @@ -57,20 +57,6 @@ function createValidationsScope(schema) { validationMap.set(id, validation); }); - function checkDataIntegrity(rule, id) { - Object.values(rule).map((subRule) => { - subRule.map((item) => { - const isVar = Object.hasOwn(item, 'var'); - if (isVar) { - const exists = jsonLogic.apply({ var: item.var }, sampleEmptyObject); - if (exists === null) { - throw Error(`"${item.var}" in rule "${id}" does not exist as a JSON schema property.`); - } - } - }); - }); - } - computedValues.forEach(([id, computedValue]) => { if (!computedValue.rule) { throw Error(`Missing rule for computedValue with id of: "${id}".`); @@ -204,3 +190,19 @@ function buildSampleEmptyObject(schema) { }) ); } + +function checkDataIntegrity(rule, id, data) { + Object.values(rule ?? {}).map((subRule) => { + subRule.map((item) => { + const isVar = item !== null && typeof item === 'object' && Object.hasOwn(item, 'var'); + if (isVar) { + const exists = jsonLogic.apply({ var: item.var }, data); + if (exists === null) { + throw Error(`"${item.var}" in rule "${id}" does not exist as a JSON schema property.`); + } + } else { + checkDataIntegrity(item, id, data); + } + }); + }); +} diff --git a/src/tests/jsonLogic.test.js b/src/tests/jsonLogic.test.js index d3b0fa72f..b37ba390d 100644 --- a/src/tests/jsonLogic.test.js +++ b/src/tests/jsonLogic.test.js @@ -157,7 +157,37 @@ describe('cross-value validations', () => { console.error.mockRestore(); }); - it.todo('Should throw when a var does not exist in a deeply nested rule'); + it('Should throw when a var does not exist in a deeply nested rule', () => { + const schema = { + properties: { + field_a: { + type: 'number', + }, + }, + 'x-jsf-logic': { + validations: { + a_greater_than_ten: { + errorMessage: 'Must be greater than 10', + rule: { + '>': [ + { var: 'field_a' }, + { '*': [2, { '/': [2, { '*': [1, { var: 'field_b' }] }] }] }, + ], + }, + }, + }, + }, + required: [], + }; + jest.spyOn(console, 'error').mockImplementation(() => {}); + + createHeadlessForm(schema, { strictInputType: false }); + expect(console.error).toHaveBeenCalledWith( + 'JSON Schema invalid!', + Error('"field_b" in rule "a_greater_than_ten" does not exist as a JSON schema property.') + ); + console.error.mockRestore(); + }); it.todo('Should throw when a var does not exist in a fieldset.'); it.todo('Should throw when a var does not exist in an array.'); From 2f2db07f3f90425681531e839f982b9d18b96049 Mon Sep 17 00:00:00 2001 From: brennj Date: Fri, 7 Jul 2023 15:37:14 +0200 Subject: [PATCH 36/69] chore: transferring schemas to own file --- src/jsonLogic.js | 8 +- src/tests/jsonLogic.test.js | 413 ++++++++++----------------------- src/tests/jsonLogicFixtures.js | 288 +++++++++++++++++++++++ 3 files changed, 421 insertions(+), 288 deletions(-) create mode 100644 src/tests/jsonLogicFixtures.js diff --git a/src/jsonLogic.js b/src/jsonLogic.js index f6ec03fac..a7505b8e4 100644 --- a/src/jsonLogic.js +++ b/src/jsonLogic.js @@ -52,7 +52,7 @@ function createValidationsScope(schema) { throw Error(`Missing rule for validation with id of: "${id}".`); } - checkDataIntegrity(validation.rule, id, sampleEmptyObject); + checkRuleIntegrity(validation.rule, id, sampleEmptyObject); validationMap.set(id, validation); }); @@ -62,6 +62,8 @@ function createValidationsScope(schema) { throw Error(`Missing rule for computedValue with id of: "${id}".`); } + checkRuleIntegrity(computedValue.rule, id, sampleEmptyObject); + computedValuesMap.set(id, computedValue); }); @@ -191,7 +193,7 @@ function buildSampleEmptyObject(schema) { ); } -function checkDataIntegrity(rule, id, data) { +function checkRuleIntegrity(rule, id, data) { Object.values(rule ?? {}).map((subRule) => { subRule.map((item) => { const isVar = item !== null && typeof item === 'object' && Object.hasOwn(item, 'var'); @@ -201,7 +203,7 @@ function checkDataIntegrity(rule, id, data) { throw Error(`"${item.var}" in rule "${id}" does not exist as a JSON schema property.`); } } else { - checkDataIntegrity(item, id, data); + checkRuleIntegrity(item, id, data); } }); }); diff --git a/src/tests/jsonLogic.test.js b/src/tests/jsonLogic.test.js index b37ba390d..82e2dae81 100644 --- a/src/tests/jsonLogic.test.js +++ b/src/tests/jsonLogic.test.js @@ -1,63 +1,24 @@ import { createHeadlessForm } from '../createHeadlessForm'; -function createSchemaWithRulesOnFieldA(rules) { - return { - properties: { - field_a: { - type: 'number', - 'x-jsf-requiredValidations': Object.keys(rules), - }, - field_b: { - type: 'number', - }, - }, - required: ['field_a', 'field_b'], - 'x-jsf-logic': { validations: rules }, - }; -} - -function createSchemaWithThreePropertiesWithRuleOnFieldA(rules) { - return { - properties: { - field_a: { - type: 'number', - 'x-jsf-requiredValidations': Object.keys(rules), - }, - field_b: { - type: 'number', - }, - field_c: { - type: 'number', - }, - }, - 'x-jsf-logic': { validations: rules }, - required: ['field_a', 'field_b', 'field_c'], - }; -} +import { + createSchemaWithRulesOnFieldA, + createSchemaWithThreePropertiesWithRuleOnFieldA, + schemaWithChecksAndThenValidationsOnThen, + schemaWithDeepVarThatDoesNotExist, + schemaWithGreaterThanChecksForThreeFields, + schemaWithMissingRule, + schemaWithNativeAndJSONLogicChecks, + schemaWithNonRequiredField, + schemaWithPropertiesCheckAndValidationsInAIf, + schemaWithVarThatDoesNotExist, +} from './jsonLogicFixtures'; describe('cross-value validations', () => { describe('Does not conflict with native JSON schema', () => { it('When a field is not required, validations should not block submitting when its an empty value', () => { - const schema = { - properties: { - field_a: { - type: 'number', - 'x-jsf-requiredValidations': ['a_greater_than_ten'], - }, - }, - 'x-jsf-logic': { - validations: { - a_greater_than_ten: { - errorMessage: 'Must be greater than 10', - rule: { - '>': [{ var: 'field_a' }, 10], - }, - }, - }, - }, - required: [], - }; - const { handleValidation } = createHeadlessForm(schema, { strictInputType: false }); + const { handleValidation } = createHeadlessForm(schemaWithNonRequiredField, { + strictInputType: false, + }); expect(handleValidation({}).formErrors).toEqual(undefined); expect(handleValidation({ field_a: 0 }).formErrors).toEqual({ field_a: 'Must be greater than 10', @@ -69,27 +30,9 @@ describe('cross-value validations', () => { }); it('Native validations always appear first', () => { - const schema = { - properties: { - field_a: { - type: 'number', - minimum: 5, - 'x-jsf-requiredValidations': ['a_greater_than_ten'], - }, - }, - 'x-jsf-logic': { - validations: { - a_greater_than_ten: { - errorMessage: 'Must be greater than 10', - rule: { - '>': [{ var: 'field_a' }, 10], - }, - }, - }, - }, - required: ['field_a'], - }; - const { handleValidation } = createHeadlessForm(schema, { strictInputType: false }); + const { handleValidation } = createHeadlessForm(schemaWithNativeAndJSONLogicChecks, { + strictInputType: false, + }); expect(handleValidation({}).formErrors).toEqual({ field_a: 'Required field' }); expect(handleValidation({ field_a: 0 }).formErrors).toEqual({ field_a: 'Must be greater or equal to 5', @@ -102,25 +45,9 @@ describe('cross-value validations', () => { describe('Badly written schemas', () => { it('Should throw when theres a missing rule', () => { - const schema = { - properties: { - field_a: { - type: 'number', - 'x-jsf-requiredValidations': ['a_greater_than_ten'], - }, - }, - 'x-jsf-logic': { - validations: { - a_greater_than_ten: { - errorMessage: 'Must be greater than 10', - }, - }, - }, - required: [], - }; jest.spyOn(console, 'error').mockImplementation(() => {}); - createHeadlessForm(schema, { strictInputType: false }); + createHeadlessForm(schemaWithMissingRule, { strictInputType: false }); expect(console.error).toHaveBeenCalledWith( 'JSON Schema invalid!', Error('Missing rule for validation with id of: "a_greater_than_ten".') @@ -129,27 +56,9 @@ describe('cross-value validations', () => { }); it('Should throw when a var does not exist in a rule.', () => { - const schema = { - properties: { - field_a: { - type: 'number', - }, - }, - 'x-jsf-logic': { - validations: { - a_greater_than_ten: { - errorMessage: 'Must be greater than 10', - rule: { - '>': [{ var: 'field_b' }, 10], - }, - }, - }, - }, - required: [], - }; jest.spyOn(console, 'error').mockImplementation(() => {}); - createHeadlessForm(schema, { strictInputType: false }); + createHeadlessForm(schemaWithVarThatDoesNotExist, { strictInputType: false }); expect(console.error).toHaveBeenCalledWith( 'JSON Schema invalid!', Error('"field_b" in rule "a_greater_than_ten" does not exist as a JSON schema property.') @@ -158,38 +67,61 @@ describe('cross-value validations', () => { }); it('Should throw when a var does not exist in a deeply nested rule', () => { + jest.spyOn(console, 'error').mockImplementation(() => {}); + createHeadlessForm(schemaWithDeepVarThatDoesNotExist, { strictInputType: false }); + expect(console.error).toHaveBeenCalledWith( + 'JSON Schema invalid!', + Error('"field_b" in rule "a_greater_than_ten" does not exist as a JSON schema property.') + ); + console.error.mockRestore(); + }); + + it.todo('Should throw when a var does not exist in a fieldset.'); + + it.todo('On a property, it should throw an error for a requiredValidation that does not exist'); + + it.skip('A top level logic keyword will not be able to reference fieldset properties', () => { const schema = { properties: { field_a: { - type: 'number', + type: 'object', + 'x-jsf-presentation': { + inputType: 'fieldset', + }, + properties: { + child: { + type: 'number', + 'x-jsf-requiredValidations': ['child_greater_than_10'], + }, + other_child: { + type: 'number', + 'x-jsf-requiredValidations': ['greater_than_child'], + }, + }, + required: ['child', 'other_child'], }, }, 'x-jsf-logic': { validations: { - a_greater_than_ten: { - errorMessage: 'Must be greater than 10', + validation_parent: { + errorMessage: 'Must be greater than 10!', rule: { - '>': [ - { var: 'field_a' }, - { '*': [2, { '/': [2, { '*': [1, { var: 'field_b' }] }] }] }, - ], + '>': [{ var: 'child' }, 10], + }, + }, + greater_than_child: { + errorMessage: 'Must be greater than child', + rule: { + '>': [{ var: 'other_child' }, { var: 'child' }], }, }, }, }, - required: [], + required: ['field_a'], }; - jest.spyOn(console, 'error').mockImplementation(() => {}); - createHeadlessForm(schema, { strictInputType: false }); - expect(console.error).toHaveBeenCalledWith( - 'JSON Schema invalid!', - Error('"field_b" in rule "a_greater_than_ten" does not exist as a JSON schema property.') - ); - console.error.mockRestore(); }); - it.todo('Should throw when a var does not exist in a fieldset.'); it.todo('Should throw when a var does not exist in an array.'); }); @@ -339,54 +271,10 @@ describe('cross-value validations', () => { describe('Conditionals', () => { it('when field_a > field_b, show field_c', () => { - const schema = { - properties: { - field_a: { - type: 'number', - }, - field_b: { - type: 'number', - }, - field_c: { - type: 'number', - }, - }, - required: ['field_a', 'field_b'], - 'x-jsf-logic': { - validations: { - require_c: { - rule: { - and: [ - { '!==': [{ var: 'field_b' }, null] }, - { '!==': [{ var: 'field_a' }, null] }, - { '>': [{ var: 'field_a' }, { var: 'field_b' }] }, - ], - }, - }, - }, - allOf: [ - { - if: { - validations: { - require_c: { - const: true, - }, - }, - }, - then: { - required: ['field_c'], - }, - else: { - properties: { - field_c: false, - }, - }, - }, - ], - }, - }; - - const { fields, handleValidation } = createHeadlessForm(schema, { strictInputType: false }); + const { fields, handleValidation } = createHeadlessForm( + schemaWithGreaterThanChecksForThreeFields, + { strictInputType: false } + ); expect(fields.find((i) => i.name === 'field_c').isVisible).toEqual(false); expect(handleValidation({ field_a: 1, field_b: 3 }).formErrors).toEqual(undefined); @@ -402,56 +290,10 @@ describe('cross-value validations', () => { }); it('A schema with both a `x-jsf-validations` and `properties` check', () => { - const schema = { - properties: { - field_a: { - type: 'number', - }, - field_b: { - type: 'number', - }, - field_c: { - type: 'number', - }, - }, - required: ['field_a', 'field_b'], - allOf: [ - { - if: { - 'x-jsf-validations': { - require_c: { - const: true, - }, - }, - properties: { - field_a: { - const: 10, - }, - }, - }, - then: { - required: ['field_c'], - }, - else: { - properties: { - field_c: false, - }, - }, - }, - ], - 'x-jsf-validations': { - require_c: { - rule: { - and: [ - { '!==': [{ var: 'field_b' }, null] }, - { '!==': [{ var: 'field_a' }, null] }, - { '>': [{ var: 'field_a' }, { var: 'field_b' }] }, - ], - }, - }, - }, - }; - const { handleValidation } = createHeadlessForm(schema, { strictInputType: false }); + const { handleValidation } = createHeadlessForm( + schemaWithPropertiesCheckAndValidationsInAIf, + { strictInputType: false } + ); expect(handleValidation({ field_a: 1, field_b: 3 }).formErrors).toEqual(undefined); expect(handleValidation({ field_a: 10, field_b: 3 }).formErrors).toEqual({ field_c: 'Required field', @@ -460,65 +302,10 @@ describe('cross-value validations', () => { }); it('Conditionally apply a validation on a property depending on values', () => { - const schema = { - properties: { - field_a: { - type: 'number', - }, - field_b: { - type: 'number', - }, - field_c: { - type: 'number', - }, - }, - required: ['field_a', 'field_b'], - 'x-jsf-logic': { - validations: { - c_must_be_large: { - errorMessage: 'Needs more numbers', - rule: { - '>': [{ var: 'field_c' }, 200], - }, - }, - require_c: { - rule: { - and: [ - { '!==': [{ var: 'field_b' }, null] }, - { '!==': [{ var: 'field_a' }, null] }, - { '>': [{ var: 'field_a' }, { var: 'field_b' }] }, - ], - }, - }, - }, - allOf: [ - { - if: { - validations: { - require_c: { - const: true, - }, - }, - }, - then: { - required: ['field_c'], - properties: { - field_c: { - description: 'I am a description!', - 'x-jsf-requiredValidations': ['c_must_be_large'], - }, - }, - }, - else: { - properties: { - field_c: false, - }, - }, - }, - ], - }, - }; - const { fields, handleValidation } = createHeadlessForm(schema, { strictInputType: false }); + const { fields, handleValidation } = createHeadlessForm( + schemaWithChecksAndThenValidationsOnThen, + { strictInputType: false } + ); const cField = fields.find((i) => i.name === 'field_c'); expect(cField.isVisible).toEqual(false); expect(cField.description).toEqual(undefined); @@ -956,7 +743,63 @@ describe('cross-value validations', () => { ); }); - it.todo('Validate a field and a nested field together'); + it.skip('Validate a field and a nested field together', () => { + const schema = { + properties: { + field_a: { + type: 'object', + 'x-jsf-presentation': { + inputType: 'fieldset', + }, + properties: { + child: { + type: 'number', + 'x-jsf-requiredValidations': ['child_greater_than_10'], + }, + other_child: { + type: 'number', + 'x-jsf-requiredValidations': ['greater_than_child'], + }, + }, + required: ['child', 'other_child'], + 'x-jsf-logic': { + validations: { + child_greater_than_10: { + errorMessage: 'Must be greater than 10!', + rule: { + '>': [{ var: 'child' }, 10], + }, + }, + greater_than_child: { + errorMessage: 'Must be greater than child', + rule: { + '>': [{ var: 'other_child' }, { var: 'child' }], + }, + }, + }, + }, + }, + }, + 'x-jsf-logic': { + validations: { + validation_parent: { + errorMessage: 'Must be greater than 10!', + rule: { + '>': [{ var: 'child' }, 10], + }, + }, + greater_than_child: { + errorMessage: 'Must be greater than child', + rule: { + '>': [{ var: 'other_child' }, { var: 'child' }], + }, + }, + }, + }, + required: ['field_a'], + }; + createHeadlessForm(schema, { strictInputType: false }); + }); it.todo('compute a nested field attribute'); }); diff --git a/src/tests/jsonLogicFixtures.js b/src/tests/jsonLogicFixtures.js new file mode 100644 index 000000000..747414ea1 --- /dev/null +++ b/src/tests/jsonLogicFixtures.js @@ -0,0 +1,288 @@ +export function createSchemaWithRulesOnFieldA(rules) { + return { + properties: { + field_a: { + type: 'number', + 'x-jsf-requiredValidations': Object.keys(rules), + }, + field_b: { + type: 'number', + }, + }, + required: ['field_a', 'field_b'], + 'x-jsf-logic': { validations: rules }, + }; +} + +export function createSchemaWithThreePropertiesWithRuleOnFieldA(rules) { + return { + properties: { + field_a: { + type: 'number', + 'x-jsf-requiredValidations': Object.keys(rules), + }, + field_b: { + type: 'number', + }, + field_c: { + type: 'number', + }, + }, + 'x-jsf-logic': { validations: rules }, + required: ['field_a', 'field_b', 'field_c'], + }; +} + +export const schemaWithNonRequiredField = { + properties: { + field_a: { + type: 'number', + 'x-jsf-requiredValidations': ['a_greater_than_ten'], + }, + }, + 'x-jsf-logic': { + validations: { + a_greater_than_ten: { + errorMessage: 'Must be greater than 10', + rule: { + '>': [{ var: 'field_a' }, 10], + }, + }, + }, + }, + required: [], +}; + +export const schemaWithNativeAndJSONLogicChecks = { + properties: { + field_a: { + type: 'number', + minimum: 5, + 'x-jsf-requiredValidations': ['a_greater_than_ten'], + }, + }, + 'x-jsf-logic': { + validations: { + a_greater_than_ten: { + errorMessage: 'Must be greater than 10', + rule: { + '>': [{ var: 'field_a' }, 10], + }, + }, + }, + }, + required: ['field_a'], +}; + +export const schemaWithMissingRule = { + properties: { + field_a: { + type: 'number', + 'x-jsf-requiredValidations': ['a_greater_than_ten'], + }, + }, + 'x-jsf-logic': { + validations: { + a_greater_than_ten: { + errorMessage: 'Must be greater than 10', + }, + }, + }, + required: [], +}; + +export const schemaWithVarThatDoesNotExist = { + properties: { + field_a: { + type: 'number', + }, + }, + 'x-jsf-logic': { + validations: { + a_greater_than_ten: { + errorMessage: 'Must be greater than 10', + rule: { + '>': [{ var: 'field_b' }, 10], + }, + }, + }, + }, + required: [], +}; + +export const schemaWithDeepVarThatDoesNotExist = { + properties: { + field_a: { + type: 'number', + }, + }, + 'x-jsf-logic': { + validations: { + a_greater_than_ten: { + errorMessage: 'Must be greater than 10', + rule: { + '>': [{ var: 'field_a' }, { '*': [2, { '/': [2, { '*': [1, { var: 'field_b' }] }] }] }], + }, + }, + }, + }, + required: [], +}; + +export const schemaWithGreaterThanChecksForThreeFields = { + properties: { + field_a: { + type: 'number', + }, + field_b: { + type: 'number', + }, + field_c: { + type: 'number', + }, + }, + required: ['field_a', 'field_b'], + 'x-jsf-logic': { + validations: { + require_c: { + rule: { + and: [ + { '!==': [{ var: 'field_b' }, null] }, + { '!==': [{ var: 'field_a' }, null] }, + { '>': [{ var: 'field_a' }, { var: 'field_b' }] }, + ], + }, + }, + }, + allOf: [ + { + if: { + validations: { + require_c: { + const: true, + }, + }, + }, + then: { + required: ['field_c'], + }, + else: { + properties: { + field_c: false, + }, + }, + }, + ], + }, +}; + +export const schemaWithPropertiesCheckAndValidationsInAIf = { + properties: { + field_a: { + type: 'number', + }, + field_b: { + type: 'number', + }, + field_c: { + type: 'number', + }, + }, + required: ['field_a', 'field_b'], + 'x-jsf-logic': { + validations: { + require_c: { + rule: { + and: [ + { '!==': [{ var: 'field_b' }, null] }, + { '!==': [{ var: 'field_a' }, null] }, + { '>': [{ var: 'field_a' }, { var: 'field_b' }] }, + ], + }, + }, + }, + allOf: [ + { + if: { + validations: { + require_c: { + const: true, + }, + }, + properties: { + field_a: { + const: 10, + }, + }, + }, + then: { + required: ['field_c'], + }, + else: { + properties: { + field_c: false, + }, + }, + }, + ], + }, +}; + +export const schemaWithChecksAndThenValidationsOnThen = { + properties: { + field_a: { + type: 'number', + }, + field_b: { + type: 'number', + }, + field_c: { + type: 'number', + }, + }, + required: ['field_a', 'field_b'], + 'x-jsf-logic': { + validations: { + c_must_be_large: { + errorMessage: 'Needs more numbers', + rule: { + '>': [{ var: 'field_c' }, 200], + }, + }, + require_c: { + rule: { + and: [ + { '!==': [{ var: 'field_b' }, null] }, + { '!==': [{ var: 'field_a' }, null] }, + { '>': [{ var: 'field_a' }, { var: 'field_b' }] }, + ], + }, + }, + }, + allOf: [ + { + if: { + validations: { + require_c: { + const: true, + }, + }, + }, + then: { + required: ['field_c'], + properties: { + field_c: { + description: 'I am a description!', + 'x-jsf-requiredValidations': ['c_must_be_large'], + }, + }, + }, + else: { + properties: { + field_c: false, + }, + }, + }, + ], + }, +}; From 056cd623d6075f52aa72a3a89a6aa8cc8753ffc8 Mon Sep 17 00:00:00 2001 From: brennj Date: Fri, 7 Jul 2023 16:08:28 +0200 Subject: [PATCH 37/69] chore: finish moving schemas to own file --- src/tests/jsonLogic.test.js | 411 +++------------------------------ src/tests/jsonLogicFixtures.js | 375 ++++++++++++++++++++++++++++++ 2 files changed, 407 insertions(+), 379 deletions(-) diff --git a/src/tests/jsonLogic.test.js b/src/tests/jsonLogic.test.js index 82e2dae81..91f66875b 100644 --- a/src/tests/jsonLogic.test.js +++ b/src/tests/jsonLogic.test.js @@ -3,14 +3,23 @@ import { createHeadlessForm } from '../createHeadlessForm'; import { createSchemaWithRulesOnFieldA, createSchemaWithThreePropertiesWithRuleOnFieldA, + multiRuleSchema, + nestedFieldsetWithValidationSchema, schemaWithChecksAndThenValidationsOnThen, + schemaWithComputedAttributes, + schemaWithComputedValueChecksInIf, schemaWithDeepVarThatDoesNotExist, schemaWithGreaterThanChecksForThreeFields, + schemaWithIfStatementWithComputedValuesAndValidationChecks, schemaWithMissingRule, + schemaWithMultipleComputedValueChecks, schemaWithNativeAndJSONLogicChecks, schemaWithNonRequiredField, schemaWithPropertiesCheckAndValidationsInAIf, + schemaWithTwoRules, schemaWithVarThatDoesNotExist, + twoLevelsOfJSONLogicSchema, + validatingTwoNestedFieldsSchema, } from './jsonLogicFixtures'; describe('cross-value validations', () => { @@ -322,53 +331,9 @@ describe('cross-value validations', () => { }); it('Should apply a conditional based on a true computedValue', () => { - const schema = { - properties: { - field_a: { - type: 'number', - }, - field_b: { - type: 'number', - }, - field_c: { - type: 'number', - }, - }, - required: ['field_a', 'field_b'], - 'x-jsf-logic': { - computedValues: { - require_c: { - rule: { - and: [ - { '!==': [{ var: 'field_b' }, null] }, - { '!==': [{ var: 'field_a' }, null] }, - { '>': [{ var: 'field_a' }, { var: 'field_b' }] }, - ], - }, - }, - }, - allOf: [ - { - if: { - computedValues: { - require_c: { - const: true, - }, - }, - }, - then: { - required: ['field_c'], - }, - else: { - properties: { - field_c: false, - }, - }, - }, - ], - }, - }; - const { fields, handleValidation } = createHeadlessForm(schema, { strictInputType: false }); + const { fields, handleValidation } = createHeadlessForm(schemaWithComputedValueChecksInIf, { + strictInputType: false, + }); const cField = fields.find((i) => i.name === 'field_c'); expect(cField.isVisible).toEqual(false); expect(cField.description).toEqual(undefined); @@ -381,71 +346,9 @@ describe('cross-value validations', () => { }); it('Handle multiple computedValue checks by ANDing them together', () => { - const schema = { - properties: { - field_a: { - type: 'number', - }, - field_b: { - type: 'number', - }, - field_c: { - type: 'number', - }, - }, - required: ['field_a', 'field_b'], - 'x-jsf-logic': { - validations: { - double_b: { - errorMessage: 'Must be two times B', - rule: { - '>': [{ var: 'field_c' }, { '*': [{ var: 'field_b' }, 2] }], - }, - }, - }, - computedValues: { - a_times_two: { - rule: { - '*': [{ var: 'field_a' }, 2], - }, - }, - mod_by_five: { - rule: { - '%': [{ var: 'field_b' }, 5], - }, - }, - }, - allOf: [ - { - if: { - computedValues: { - a_times_two: { - const: 20, - }, - mod_by_five: { - const: 3, - }, - }, - }, - then: { - required: ['field_c'], - properties: { - field_c: { - 'x-jsf-requiredValidations': ['double_b'], - title: 'Adding a title.', - }, - }, - }, - else: { - properties: { - field_c: false, - }, - }, - }, - ], - }, - }; - const { handleValidation } = createHeadlessForm(schema, { strictInputType: false }); + const { handleValidation } = createHeadlessForm(schemaWithMultipleComputedValueChecks, { + strictInputType: false, + }); expect(handleValidation({}).formErrors).toEqual({ field_a: 'Required field', field_b: 'Required field', @@ -462,61 +365,10 @@ describe('cross-value validations', () => { }); it('Handle having a true condition with both validations and computedValue checks', () => { - const schema = { - properties: { - field_a: { - type: 'number', - }, - field_b: { - type: 'number', - }, - field_c: { - type: 'number', - }, - }, - required: ['field_a', 'field_b'], - 'x-jsf-logic': { - validations: { - greater_than_b: { - rule: { - '>': [{ var: 'field_a' }, { var: 'field_b' }], - }, - }, - }, - computedValues: { - a_times_two: { - rule: { - '*': [{ var: 'field_a' }, 2], - }, - }, - }, - allOf: [ - { - if: { - computedValues: { - a_times_two: { - const: 20, - }, - }, - validations: { - greater_than_b: { - const: true, - }, - }, - }, - then: { - required: ['field_c'], - }, - else: { - properties: { - field_c: false, - }, - }, - }, - ], - }, - }; - const { handleValidation } = createHeadlessForm(schema, { strictInputType: false }); + const { handleValidation } = createHeadlessForm( + schemaWithIfStatementWithComputedValuesAndValidationChecks, + { strictInputType: false } + ); expect(handleValidation({ field_a: 1, field_b: 1 }).formErrors).toEqual(undefined); expect(handleValidation({ field_a: 10, field_b: 20 }).formErrors).toEqual(undefined); expect(handleValidation({ field_a: 10, field_b: 9 }).formErrors).toEqual({ @@ -530,36 +382,7 @@ describe('cross-value validations', () => { describe('Multiple validations', () => { it('2 rules where A must be bigger than B and not an even number in another rule', () => { - const schema = { - properties: { - field_a: { - type: 'number', - 'x-jsf-requiredValidations': ['a_bigger_than_b', 'is_even_number'], - }, - field_b: { - type: 'number', - }, - }, - required: ['field_a', 'field_b'], - 'x-jsf-logic': { - validations: { - a_bigger_than_b: { - errorMessage: 'A must be bigger than B', - rule: { - '>': [{ var: 'field_a' }, { var: 'field_b' }], - }, - }, - is_even_number: { - errorMessage: 'A must be even', - rule: { - '===': [{ '%': [{ var: 'field_a' }, 2] }, 0], - }, - }, - }, - }, - }; - - const { handleValidation } = createHeadlessForm(schema, { strictInputType: false }); + const { handleValidation } = createHeadlessForm(multiRuleSchema, { strictInputType: false }); expect(handleValidation({ field_a: 1 }).formErrors).toEqual({ field_a: 'A must be even', field_b: 'Required field', @@ -574,37 +397,9 @@ describe('cross-value validations', () => { }); it('2 seperate fields with rules failing', () => { - const schema = { - properties: { - field_a: { - type: 'number', - 'x-jsf-requiredValidations': ['a_bigger_than_b'], - }, - field_b: { - type: 'number', - 'x-jsf-requiredValidations': ['is_even_number'], - }, - }, - required: ['field_a', 'field_b'], - 'x-jsf-logic': { - validations: { - a_bigger_than_b: { - errorMessage: 'A must be bigger than B', - rule: { - '>': [{ var: 'field_a' }, { var: 'field_b' }], - }, - }, - is_even_number: { - errorMessage: 'B must be even', - rule: { - '===': [{ '%': [{ var: 'field_b' }, 2] }, 0], - }, - }, - }, - }, - }; - - const { handleValidation } = createHeadlessForm(schema, { strictInputType: false }); + const { handleValidation } = createHeadlessForm(schemaWithTwoRules, { + strictInputType: false, + }); expect(handleValidation({ field_a: 1, field_b: 3 }).formErrors).toEqual({ field_a: 'A must be bigger than B', field_b: 'B must be even', @@ -615,33 +410,7 @@ describe('cross-value validations', () => { describe('Derive values', () => { it('field_b is field_a * 2', () => { - const schema = { - properties: { - field_a: { - type: 'number', - }, - field_b: { - type: 'number', - 'x-jsf-computedAttributes': { - title: 'This is {{a_times_two}}!', - value: 'a_times_two', - description: - 'This field is 2 times bigger than field_a with value of {{a_times_two}}.', - }, - }, - }, - required: ['field_a', 'field_b'], - 'x-jsf-logic': { - computedValues: { - a_times_two: { - rule: { - '*': [{ var: 'field_a' }, 2], - }, - }, - }, - }, - }; - const { fields } = createHeadlessForm(schema, { + const { fields } = createHeadlessForm(schemaWithComputedAttributes, { strictInputType: false, initialValues: { field_a: 2 }, }); @@ -656,35 +425,9 @@ describe('cross-value validations', () => { describe('Nested fieldsets', () => { it('Basic nested validation works', () => { - const schema = { - properties: { - field_a: { - type: 'object', - 'x-jsf-presentation': { - inputType: 'fieldset', - }, - properties: { - child: { - type: 'number', - 'x-jsf-requiredValidations': ['child_greater_than_10'], - }, - }, - required: ['child'], - 'x-jsf-logic': { - validations: { - child_greater_than_10: { - errorMessage: 'Must be greater than 10!', - rule: { - '>': [{ var: 'child' }, 10], - }, - }, - }, - }, - }, - }, - required: ['field_a'], - }; - const { handleValidation } = createHeadlessForm(schema, { strictInputType: false }); + const { handleValidation } = createHeadlessForm(nestedFieldsetWithValidationSchema, { + strictInputType: false, + }); expect(handleValidation({}).formErrors).toEqual({ field_a: { child: 'Required field' } }); expect(handleValidation({ field_a: { child: 0 } }).formErrors).toEqual({ field_a: { child: 'Must be greater than 10!' }, @@ -693,45 +436,9 @@ describe('cross-value validations', () => { }); it('Validating two nested fields together', () => { - const schema = { - properties: { - field_a: { - type: 'object', - 'x-jsf-presentation': { - inputType: 'fieldset', - }, - properties: { - child: { - type: 'number', - 'x-jsf-requiredValidations': ['child_greater_than_10'], - }, - other_child: { - type: 'number', - 'x-jsf-requiredValidations': ['greater_than_child'], - }, - }, - required: ['child', 'other_child'], - 'x-jsf-logic': { - validations: { - child_greater_than_10: { - errorMessage: 'Must be greater than 10!', - rule: { - '>': [{ var: 'child' }, 10], - }, - }, - greater_than_child: { - errorMessage: 'Must be greater than child', - rule: { - '>': [{ var: 'other_child' }, { var: 'child' }], - }, - }, - }, - }, - }, - }, - required: ['field_a'], - }; - const { handleValidation } = createHeadlessForm(schema, { strictInputType: false }); + const { handleValidation } = createHeadlessForm(validatingTwoNestedFieldsSchema, { + strictInputType: false, + }); expect(handleValidation({}).formErrors).toEqual({ field_a: { child: 'Required field', other_child: 'Required field' }, }); @@ -743,62 +450,8 @@ describe('cross-value validations', () => { ); }); - it.skip('Validate a field and a nested field together', () => { - const schema = { - properties: { - field_a: { - type: 'object', - 'x-jsf-presentation': { - inputType: 'fieldset', - }, - properties: { - child: { - type: 'number', - 'x-jsf-requiredValidations': ['child_greater_than_10'], - }, - other_child: { - type: 'number', - 'x-jsf-requiredValidations': ['greater_than_child'], - }, - }, - required: ['child', 'other_child'], - 'x-jsf-logic': { - validations: { - child_greater_than_10: { - errorMessage: 'Must be greater than 10!', - rule: { - '>': [{ var: 'child' }, 10], - }, - }, - greater_than_child: { - errorMessage: 'Must be greater than child', - rule: { - '>': [{ var: 'other_child' }, { var: 'child' }], - }, - }, - }, - }, - }, - }, - 'x-jsf-logic': { - validations: { - validation_parent: { - errorMessage: 'Must be greater than 10!', - rule: { - '>': [{ var: 'child' }, 10], - }, - }, - greater_than_child: { - errorMessage: 'Must be greater than child', - rule: { - '>': [{ var: 'other_child' }, { var: 'child' }], - }, - }, - }, - }, - required: ['field_a'], - }; - createHeadlessForm(schema, { strictInputType: false }); + it('Validate a field and a nested field together', () => { + createHeadlessForm(twoLevelsOfJSONLogicSchema, { strictInputType: false }); }); it.todo('compute a nested field attribute'); }); diff --git a/src/tests/jsonLogicFixtures.js b/src/tests/jsonLogicFixtures.js index 747414ea1..ffd922f26 100644 --- a/src/tests/jsonLogicFixtures.js +++ b/src/tests/jsonLogicFixtures.js @@ -286,3 +286,378 @@ export const schemaWithChecksAndThenValidationsOnThen = { ], }, }; + +export const schemaWithComputedValueChecksInIf = { + properties: { + field_a: { + type: 'number', + }, + field_b: { + type: 'number', + }, + field_c: { + type: 'number', + }, + }, + required: ['field_a', 'field_b'], + 'x-jsf-logic': { + computedValues: { + require_c: { + rule: { + and: [ + { '!==': [{ var: 'field_b' }, null] }, + { '!==': [{ var: 'field_a' }, null] }, + { '>': [{ var: 'field_a' }, { var: 'field_b' }] }, + ], + }, + }, + }, + allOf: [ + { + if: { + computedValues: { + require_c: { + const: true, + }, + }, + }, + then: { + required: ['field_c'], + }, + else: { + properties: { + field_c: false, + }, + }, + }, + ], + }, +}; + +export const schemaWithMultipleComputedValueChecks = { + properties: { + field_a: { + type: 'number', + }, + field_b: { + type: 'number', + }, + field_c: { + type: 'number', + }, + }, + required: ['field_a', 'field_b'], + 'x-jsf-logic': { + validations: { + double_b: { + errorMessage: 'Must be two times B', + rule: { + '>': [{ var: 'field_c' }, { '*': [{ var: 'field_b' }, 2] }], + }, + }, + }, + computedValues: { + a_times_two: { + rule: { + '*': [{ var: 'field_a' }, 2], + }, + }, + mod_by_five: { + rule: { + '%': [{ var: 'field_b' }, 5], + }, + }, + }, + allOf: [ + { + if: { + computedValues: { + a_times_two: { + const: 20, + }, + mod_by_five: { + const: 3, + }, + }, + }, + then: { + required: ['field_c'], + properties: { + field_c: { + 'x-jsf-requiredValidations': ['double_b'], + title: 'Adding a title.', + }, + }, + }, + else: { + properties: { + field_c: false, + }, + }, + }, + ], + }, +}; + +export const schemaWithIfStatementWithComputedValuesAndValidationChecks = { + properties: { + field_a: { + type: 'number', + }, + field_b: { + type: 'number', + }, + field_c: { + type: 'number', + }, + }, + required: ['field_a', 'field_b'], + 'x-jsf-logic': { + validations: { + greater_than_b: { + rule: { + '>': [{ var: 'field_a' }, { var: 'field_b' }], + }, + }, + }, + computedValues: { + a_times_two: { + rule: { + '*': [{ var: 'field_a' }, 2], + }, + }, + }, + allOf: [ + { + if: { + computedValues: { + a_times_two: { + const: 20, + }, + }, + validations: { + greater_than_b: { + const: true, + }, + }, + }, + then: { + required: ['field_c'], + }, + else: { + properties: { + field_c: false, + }, + }, + }, + ], + }, +}; + +export const multiRuleSchema = { + properties: { + field_a: { + type: 'number', + 'x-jsf-requiredValidations': ['a_bigger_than_b', 'is_even_number'], + }, + field_b: { + type: 'number', + }, + }, + required: ['field_a', 'field_b'], + 'x-jsf-logic': { + validations: { + a_bigger_than_b: { + errorMessage: 'A must be bigger than B', + rule: { + '>': [{ var: 'field_a' }, { var: 'field_b' }], + }, + }, + is_even_number: { + errorMessage: 'A must be even', + rule: { + '===': [{ '%': [{ var: 'field_a' }, 2] }, 0], + }, + }, + }, + }, +}; + +export const schemaWithTwoRules = { + properties: { + field_a: { + type: 'number', + 'x-jsf-requiredValidations': ['a_bigger_than_b'], + }, + field_b: { + type: 'number', + 'x-jsf-requiredValidations': ['is_even_number'], + }, + }, + required: ['field_a', 'field_b'], + 'x-jsf-logic': { + validations: { + a_bigger_than_b: { + errorMessage: 'A must be bigger than B', + rule: { + '>': [{ var: 'field_a' }, { var: 'field_b' }], + }, + }, + is_even_number: { + errorMessage: 'B must be even', + rule: { + '===': [{ '%': [{ var: 'field_b' }, 2] }, 0], + }, + }, + }, + }, +}; + +export const schemaWithComputedAttributes = { + properties: { + field_a: { + type: 'number', + }, + field_b: { + type: 'number', + 'x-jsf-computedAttributes': { + title: 'This is {{a_times_two}}!', + value: 'a_times_two', + description: 'This field is 2 times bigger than field_a with value of {{a_times_two}}.', + }, + }, + }, + required: ['field_a', 'field_b'], + 'x-jsf-logic': { + computedValues: { + a_times_two: { + rule: { + '*': [{ var: 'field_a' }, 2], + }, + }, + }, + }, +}; + +export const nestedFieldsetWithValidationSchema = { + properties: { + field_a: { + type: 'object', + 'x-jsf-presentation': { + inputType: 'fieldset', + }, + properties: { + child: { + type: 'number', + 'x-jsf-requiredValidations': ['child_greater_than_10'], + }, + }, + required: ['child'], + 'x-jsf-logic': { + validations: { + child_greater_than_10: { + errorMessage: 'Must be greater than 10!', + rule: { + '>': [{ var: 'child' }, 10], + }, + }, + }, + }, + }, + }, + required: ['field_a'], +}; + +export const validatingTwoNestedFieldsSchema = { + properties: { + field_a: { + type: 'object', + 'x-jsf-presentation': { + inputType: 'fieldset', + }, + properties: { + child: { + type: 'number', + 'x-jsf-requiredValidations': ['child_greater_than_10'], + }, + other_child: { + type: 'number', + 'x-jsf-requiredValidations': ['greater_than_child'], + }, + }, + required: ['child', 'other_child'], + 'x-jsf-logic': { + validations: { + child_greater_than_10: { + errorMessage: 'Must be greater than 10!', + rule: { + '>': [{ var: 'child' }, 10], + }, + }, + greater_than_child: { + errorMessage: 'Must be greater than child', + rule: { + '>': [{ var: 'other_child' }, { var: 'child' }], + }, + }, + }, + }, + }, + }, + required: ['field_a'], +}; + +export const twoLevelsOfJSONLogicSchema = { + properties: { + field_a: { + type: 'object', + 'x-jsf-presentation': { + inputType: 'fieldset', + }, + properties: { + child: { + type: 'number', + 'x-jsf-requiredValidations': ['child_greater_than_10'], + }, + other_child: { + type: 'number', + 'x-jsf-requiredValidations': ['greater_than_child'], + }, + }, + required: ['child', 'other_child'], + 'x-jsf-logic': { + validations: { + child_greater_than_10: { + errorMessage: 'Must be greater than 10!', + rule: { + '>': [{ var: 'child' }, 10], + }, + }, + greater_than_child: { + errorMessage: 'Must be greater than child', + rule: { + '>': [{ var: 'other_child' }, { var: 'child' }], + }, + }, + }, + }, + }, + }, + 'x-jsf-logic': { + validations: { + validation_parent: { + errorMessage: 'Must be greater than 10!', + rule: { + '>': [{ var: 'child' }, 10], + }, + }, + greater_than_child: { + errorMessage: 'Must be greater than child', + rule: { + '>': [{ var: 'other_child' }, { var: 'child' }], + }, + }, + }, + }, + required: ['field_a'], +}; From b48160ea8cc3f9cefb584b6300687aeb9a7edbc1 Mon Sep 17 00:00:00 2001 From: brennj Date: Fri, 7 Jul 2023 16:34:26 +0200 Subject: [PATCH 38/69] chore: top level can look in nested fields --- src/tests/jsonLogic.test.js | 19 ++++++++++++++++++- src/tests/jsonLogicFixtures.js | 26 ++++++++++---------------- 2 files changed, 28 insertions(+), 17 deletions(-) diff --git a/src/tests/jsonLogic.test.js b/src/tests/jsonLogic.test.js index 91f66875b..2ca643bde 100644 --- a/src/tests/jsonLogic.test.js +++ b/src/tests/jsonLogic.test.js @@ -451,8 +451,25 @@ describe('cross-value validations', () => { }); it('Validate a field and a nested field together', () => { - createHeadlessForm(twoLevelsOfJSONLogicSchema, { strictInputType: false }); + const { handleValidation } = createHeadlessForm(twoLevelsOfJSONLogicSchema, { + strictInputType: false, + }); + expect(handleValidation({}).formErrors).toEqual({ + field_a: { child: 'Required field' }, + field_b: 'Required field', + }); + expect(handleValidation({ field_a: { child: 0 }, field_b: 0 }).formErrors).toEqual({ + field_a: { child: 'Must be greater than 10!' }, + field_b: 'Must be greater than 10!', + }); + expect(handleValidation({ field_a: { child: 11 }, field_b: 11 }).formErrors).toEqual({ + field_b: 'child must be greater than 15!', + }); + expect(handleValidation({ field_a: { child: 16 }, field_b: 11 }).formErrors).toEqual( + undefined + ); }); + it.todo('compute a nested field attribute'); }); diff --git a/src/tests/jsonLogicFixtures.js b/src/tests/jsonLogicFixtures.js index ffd922f26..3b4760d7a 100644 --- a/src/tests/jsonLogicFixtures.js +++ b/src/tests/jsonLogicFixtures.js @@ -619,12 +619,8 @@ export const twoLevelsOfJSONLogicSchema = { type: 'number', 'x-jsf-requiredValidations': ['child_greater_than_10'], }, - other_child: { - type: 'number', - 'x-jsf-requiredValidations': ['greater_than_child'], - }, }, - required: ['child', 'other_child'], + required: ['child'], 'x-jsf-logic': { validations: { child_greater_than_10: { @@ -633,31 +629,29 @@ export const twoLevelsOfJSONLogicSchema = { '>': [{ var: 'child' }, 10], }, }, - greater_than_child: { - errorMessage: 'Must be greater than child', - rule: { - '>': [{ var: 'other_child' }, { var: 'child' }], - }, - }, }, }, }, + field_b: { + type: 'number', + 'x-jsf-requiredValidations': ['validation_parent', 'peek_to_nested'], + }, }, 'x-jsf-logic': { validations: { validation_parent: { errorMessage: 'Must be greater than 10!', rule: { - '>': [{ var: 'child' }, 10], + '>': [{ var: 'field_b' }, 10], }, }, - greater_than_child: { - errorMessage: 'Must be greater than child', + peek_to_nested: { + errorMessage: 'child must be greater than 15!', rule: { - '>': [{ var: 'other_child' }, { var: 'child' }], + '>': [{ var: 'field_a.child' }, 15], }, }, }, }, - required: ['field_a'], + required: ['field_a', 'field_b'], }; From 61b23433dfd3f039c47ddcaa1da0a1433756fa11 Mon Sep 17 00:00:00 2001 From: brennj Date: Fri, 7 Jul 2023 16:37:13 +0200 Subject: [PATCH 39/69] chore: fix skipped test --- src/tests/jsonLogic.test.js | 51 ++++++++-------------------------- src/tests/jsonLogicFixtures.js | 39 ++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 40 deletions(-) diff --git a/src/tests/jsonLogic.test.js b/src/tests/jsonLogic.test.js index 2ca643bde..aa3522995 100644 --- a/src/tests/jsonLogic.test.js +++ b/src/tests/jsonLogic.test.js @@ -16,6 +16,7 @@ import { schemaWithNativeAndJSONLogicChecks, schemaWithNonRequiredField, schemaWithPropertiesCheckAndValidationsInAIf, + schemaWithPropertyThatDoesNotExistInThatLevelButDoesInFieldset, schemaWithTwoRules, schemaWithVarThatDoesNotExist, twoLevelsOfJSONLogicSchema, @@ -89,46 +90,16 @@ describe('cross-value validations', () => { it.todo('On a property, it should throw an error for a requiredValidation that does not exist'); - it.skip('A top level logic keyword will not be able to reference fieldset properties', () => { - const schema = { - properties: { - field_a: { - type: 'object', - 'x-jsf-presentation': { - inputType: 'fieldset', - }, - properties: { - child: { - type: 'number', - 'x-jsf-requiredValidations': ['child_greater_than_10'], - }, - other_child: { - type: 'number', - 'x-jsf-requiredValidations': ['greater_than_child'], - }, - }, - required: ['child', 'other_child'], - }, - }, - 'x-jsf-logic': { - validations: { - validation_parent: { - errorMessage: 'Must be greater than 10!', - rule: { - '>': [{ var: 'child' }, 10], - }, - }, - greater_than_child: { - errorMessage: 'Must be greater than child', - rule: { - '>': [{ var: 'other_child' }, { var: 'child' }], - }, - }, - }, - }, - required: ['field_a'], - }; - createHeadlessForm(schema, { strictInputType: false }); + it('A top level logic keyword will not be able to reference fieldset properties', () => { + jest.spyOn(console, 'error').mockImplementation(() => {}); + createHeadlessForm(schemaWithPropertyThatDoesNotExistInThatLevelButDoesInFieldset, { + strictInputType: false, + }); + expect(console.error).toHaveBeenCalledWith( + 'JSON Schema invalid!', + Error('"child" in rule "validation_parent" does not exist as a JSON schema property.') + ); + console.error.mockRestore(); }); it.todo('Should throw when a var does not exist in an array.'); diff --git a/src/tests/jsonLogicFixtures.js b/src/tests/jsonLogicFixtures.js index 3b4760d7a..2474b9615 100644 --- a/src/tests/jsonLogicFixtures.js +++ b/src/tests/jsonLogicFixtures.js @@ -655,3 +655,42 @@ export const twoLevelsOfJSONLogicSchema = { }, required: ['field_a', 'field_b'], }; + +export const schemaWithPropertyThatDoesNotExistInThatLevelButDoesInFieldset = { + properties: { + field_a: { + type: 'object', + 'x-jsf-presentation': { + inputType: 'fieldset', + }, + properties: { + child: { + type: 'number', + 'x-jsf-requiredValidations': ['child_greater_than_10'], + }, + other_child: { + type: 'number', + 'x-jsf-requiredValidations': ['greater_than_child'], + }, + }, + required: ['child', 'other_child'], + }, + }, + 'x-jsf-logic': { + validations: { + validation_parent: { + errorMessage: 'Must be greater than 10!', + rule: { + '>': [{ var: 'child' }, 10], + }, + }, + greater_than_child: { + errorMessage: 'Must be greater than child', + rule: { + '>': [{ var: 'other_child' }, { var: 'child' }], + }, + }, + }, + }, + required: ['field_a'], +}; From 6b9cfc1fdea15c44b81a449e0d33e6ab26f15ce7 Mon Sep 17 00:00:00 2001 From: brennj Date: Fri, 7 Jul 2023 16:41:56 +0200 Subject: [PATCH 40/69] chore: fix other test --- src/jsonLogic.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/jsonLogic.js b/src/jsonLogic.js index a7505b8e4..e317d5f5f 100644 --- a/src/jsonLogic.js +++ b/src/jsonLogic.js @@ -183,7 +183,7 @@ export function processJSONLogicNode({ node, formFields, formValues, accRequired return { required: requiredFields }; } -function buildSampleEmptyObject(schema) { +function buildSampleEmptyObject(schema = {}) { const { properties } = schema; return Object.fromEntries( Object.entries(properties ?? {}).map(([key, value]) => { From 4cde6d310b5f2f1cd54b38e22ad938ffa637c704 Mon Sep 17 00:00:00 2001 From: brennj Date: Mon, 10 Jul 2023 10:39:31 +0200 Subject: [PATCH 41/69] chore: deep var missing in fieldset test --- src/tests/jsonLogic.test.js | 25 +++++++++++++++---------- src/tests/jsonLogicFixtures.js | 24 ++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 10 deletions(-) diff --git a/src/tests/jsonLogic.test.js b/src/tests/jsonLogic.test.js index aa3522995..55d1933f0 100644 --- a/src/tests/jsonLogic.test.js +++ b/src/tests/jsonLogic.test.js @@ -9,6 +9,7 @@ import { schemaWithComputedAttributes, schemaWithComputedValueChecksInIf, schemaWithDeepVarThatDoesNotExist, + schemaWithDeepVarThatDoesNotExistOnFieldset, schemaWithGreaterThanChecksForThreeFields, schemaWithIfStatementWithComputedValuesAndValidationChecks, schemaWithMissingRule, @@ -54,44 +55,49 @@ describe('cross-value validations', () => { }); describe('Badly written schemas', () => { - it('Should throw when theres a missing rule', () => { + beforeEach(() => { jest.spyOn(console, 'error').mockImplementation(() => {}); + }); + + afterEach(() => { + console.error.mockRestore(); + }); + it('Should throw when theres a missing rule', () => { createHeadlessForm(schemaWithMissingRule, { strictInputType: false }); expect(console.error).toHaveBeenCalledWith( 'JSON Schema invalid!', Error('Missing rule for validation with id of: "a_greater_than_ten".') ); - console.error.mockRestore(); }); it('Should throw when a var does not exist in a rule.', () => { - jest.spyOn(console, 'error').mockImplementation(() => {}); - createHeadlessForm(schemaWithVarThatDoesNotExist, { strictInputType: false }); expect(console.error).toHaveBeenCalledWith( 'JSON Schema invalid!', Error('"field_b" in rule "a_greater_than_ten" does not exist as a JSON schema property.') ); - console.error.mockRestore(); }); it('Should throw when a var does not exist in a deeply nested rule', () => { - jest.spyOn(console, 'error').mockImplementation(() => {}); createHeadlessForm(schemaWithDeepVarThatDoesNotExist, { strictInputType: false }); expect(console.error).toHaveBeenCalledWith( 'JSON Schema invalid!', Error('"field_b" in rule "a_greater_than_ten" does not exist as a JSON schema property.') ); - console.error.mockRestore(); }); - it.todo('Should throw when a var does not exist in a fieldset.'); + it('Should throw when a var does not exist in a fieldset.', () => { + createHeadlessForm(schemaWithDeepVarThatDoesNotExistOnFieldset, { strictInputType: false }); + expect(console.error).toHaveBeenCalledWith( + 'JSON Schema invalid!', + Error('"field_a" in rule "a_greater_than_ten" does not exist as a JSON schema property.') + ); + }); it.todo('On a property, it should throw an error for a requiredValidation that does not exist'); it('A top level logic keyword will not be able to reference fieldset properties', () => { - jest.spyOn(console, 'error').mockImplementation(() => {}); createHeadlessForm(schemaWithPropertyThatDoesNotExistInThatLevelButDoesInFieldset, { strictInputType: false, }); @@ -99,7 +105,6 @@ describe('cross-value validations', () => { 'JSON Schema invalid!', Error('"child" in rule "validation_parent" does not exist as a JSON schema property.') ); - console.error.mockRestore(); }); it.todo('Should throw when a var does not exist in an array.'); diff --git a/src/tests/jsonLogicFixtures.js b/src/tests/jsonLogicFixtures.js index 2474b9615..210966c77 100644 --- a/src/tests/jsonLogicFixtures.js +++ b/src/tests/jsonLogicFixtures.js @@ -129,6 +129,30 @@ export const schemaWithDeepVarThatDoesNotExist = { required: [], }; +export const schemaWithDeepVarThatDoesNotExistOnFieldset = { + properties: { + field_a: { + type: 'object', + properties: { + child: { + type: 'number', + }, + }, + 'x-jsf-logic': { + validations: { + a_greater_than_ten: { + errorMessage: 'Must be greater than 10', + rule: { + '>': [{ var: 'child' }, { '*': [2, { '/': [2, { '*': [1, { var: 'field_a' }] }] }] }], + }, + }, + }, + }, + }, + }, + required: [], +}; + export const schemaWithGreaterThanChecksForThreeFields = { properties: { field_a: { From d5b0dcd54421eb9e9005b2e6b3f058bd4bf2d91e Mon Sep 17 00:00:00 2001 From: brennj Date: Mon, 10 Jul 2023 10:53:10 +0200 Subject: [PATCH 42/69] chore: throw on validations that do not exist --- src/createHeadlessForm.js | 1 + src/jsonLogic.js | 4 ++++ src/tests/jsonLogic.test.js | 11 ++++++++++- src/tests/jsonLogicFixtures.js | 9 +++++++++ src/yupSchema.js | 2 +- 5 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/createHeadlessForm.js b/src/createHeadlessForm.js index 7c6335bda..15a7a00e4 100644 --- a/src/createHeadlessForm.js +++ b/src/createHeadlessForm.js @@ -112,6 +112,7 @@ function buildFieldParameters(name, fieldProperties, required = [], config = {}, fieldProperties, { customProperties: get(config, `customProperties.${name}`, {}), + parentID: name, }, validations ); diff --git a/src/jsonLogic.js b/src/jsonLogic.js index e317d5f5f..0e054eb67 100644 --- a/src/jsonLogic.js +++ b/src/jsonLogic.js @@ -93,6 +93,10 @@ export function yupSchemaWithCustomJSONLogic({ field, validations, config, id }) const { parentID = 'root' } = config; const validation = validations.getScope(parentID).validationMap.get(id); + if (validation === undefined) { + throw Error(`Validation "${id}" required for "${field.name}" doesn't exist.`); + } + return (yupSchema) => yupSchema.test( `${field.name}-validation-${id}`, diff --git a/src/tests/jsonLogic.test.js b/src/tests/jsonLogic.test.js index 55d1933f0..e99576a29 100644 --- a/src/tests/jsonLogic.test.js +++ b/src/tests/jsonLogic.test.js @@ -19,6 +19,7 @@ import { schemaWithPropertiesCheckAndValidationsInAIf, schemaWithPropertyThatDoesNotExistInThatLevelButDoesInFieldset, schemaWithTwoRules, + schemaWithValidationThatDoesNotExistOnProperty, schemaWithVarThatDoesNotExist, twoLevelsOfJSONLogicSchema, validatingTwoNestedFieldsSchema, @@ -95,7 +96,15 @@ describe('cross-value validations', () => { ); }); - it.todo('On a property, it should throw an error for a requiredValidation that does not exist'); + it('On a property, it should throw an error for a requiredValidation that does not exist', () => { + createHeadlessForm(schemaWithValidationThatDoesNotExistOnProperty, { + strictInputType: false, + }); + expect(console.error).toHaveBeenCalledWith( + 'JSON Schema invalid!', + Error(`Validation "iDontExist" required for "field_a" doesn't exist.`) + ); + }); it('A top level logic keyword will not be able to reference fieldset properties', () => { createHeadlessForm(schemaWithPropertyThatDoesNotExistInThatLevelButDoesInFieldset, { diff --git a/src/tests/jsonLogicFixtures.js b/src/tests/jsonLogicFixtures.js index 210966c77..adf54dbe8 100644 --- a/src/tests/jsonLogicFixtures.js +++ b/src/tests/jsonLogicFixtures.js @@ -153,6 +153,15 @@ export const schemaWithDeepVarThatDoesNotExistOnFieldset = { required: [], }; +export const schemaWithValidationThatDoesNotExistOnProperty = { + properties: { + field_a: { + type: 'number', + 'x-jsf-requiredValidations': ['iDontExist'], + }, + }, +}; + export const schemaWithGreaterThanChecksForThreeFields = { properties: { field_a: { diff --git a/src/yupSchema.js b/src/yupSchema.js index 5397e2b77..095b2b512 100644 --- a/src/yupSchema.js +++ b/src/yupSchema.js @@ -272,7 +272,7 @@ export function buildYupSchema(field, config, validations) { ...fieldSetfield, inputType: fieldSetfield.type, }, - config, + { ...config, parentID: field.name }, validations )(); } From 0cc992682a53299a5306da0d5fefef2d15217c7f Mon Sep 17 00:00:00 2001 From: brennj Date: Mon, 10 Jul 2023 11:15:34 +0200 Subject: [PATCH 43/69] chore: fix nested field attribute calc --- src/createHeadlessForm.js | 2 +- src/jsonLogic.js | 7 +++++-- src/tests/jsonLogic.test.js | 31 ++++++++++++++++++++++++++++--- src/tests/jsonLogicFixtures.js | 34 ++++++++++++++++++++++++++++++++++ 4 files changed, 68 insertions(+), 6 deletions(-) diff --git a/src/createHeadlessForm.js b/src/createHeadlessForm.js index 15a7a00e4..b31a9d72f 100644 --- a/src/createHeadlessForm.js +++ b/src/createHeadlessForm.js @@ -248,7 +248,7 @@ function buildField(fieldParams, config, scopedJsonSchema, validations) { ); const caclulateComputedAttributes = - fieldParams.computedAttributes && calculateComputedAttributes(fieldParams); + fieldParams.computedAttributes && calculateComputedAttributes(fieldParams, config); const hasCustomValidations = !!customProperties && diff --git a/src/jsonLogic.js b/src/jsonLogic.js index 0e054eb67..89802dd1c 100644 --- a/src/jsonLogic.js +++ b/src/jsonLogic.js @@ -114,7 +114,7 @@ function replaceHandlebarsTemplates(string, validations, formValues) { }); } -export function calculateComputedAttributes(fieldParams) { +export function calculateComputedAttributes(fieldParams, { parentID = 'root' } = {}) { return ({ validations, formValues }) => { const { computedAttributes } = fieldParams; return Object.fromEntries( @@ -126,7 +126,10 @@ export function calculateComputedAttributes(fieldParams) { return ['label', replaceHandlebarsTemplates(value, validations, formValues)]; } if (key === 'const' || key === 'value') - return [key, validations.getScope().evaluateComputedValueRule(value, formValues)]; + return [ + key, + validations.getScope(parentID).evaluateComputedValueRule(value, formValues), + ]; return [key, null]; }) .filter(([, value]) => value !== null) diff --git a/src/tests/jsonLogic.test.js b/src/tests/jsonLogic.test.js index e99576a29..b93f61624 100644 --- a/src/tests/jsonLogic.test.js +++ b/src/tests/jsonLogic.test.js @@ -3,6 +3,7 @@ import { createHeadlessForm } from '../createHeadlessForm'; import { createSchemaWithRulesOnFieldA, createSchemaWithThreePropertiesWithRuleOnFieldA, + fieldsetWithComputedAttributes, multiRuleSchema, nestedFieldsetWithValidationSchema, schemaWithChecksAndThenValidationsOnThen, @@ -55,7 +56,7 @@ describe('cross-value validations', () => { }); }); - describe('Badly written schemas', () => { + describe('Incorrectly written schemas', () => { beforeEach(() => { jest.spyOn(console, 'error').mockImplementation(() => {}); }); @@ -116,7 +117,11 @@ describe('cross-value validations', () => { ); }); - it.todo('Should throw when a var does not exist in an array.'); + it.todo('On x-jsf-computedAttributes, error if theres a value that does not exist.'); + it.todo( + 'On x-jsf-computedAttributes, error if theres a value that does not exist on a description.' + ); + it.todo('On x-jsf-computedAttributes, error if theres a value that does not exist on a title.'); }); describe('Relative: <, >, =', () => { @@ -455,7 +460,27 @@ describe('cross-value validations', () => { ); }); - it.todo('compute a nested field attribute'); + it('compute a nested field attribute', () => { + const { fields, handleValidation } = createHeadlessForm(fieldsetWithComputedAttributes, { + strictInputType: false, + }); + const [fieldA] = fields; + const [, computedField] = fieldA.fields; + expect(handleValidation({}).formErrors).toEqual({ + field_a: { child: 'Required field' }, + }); + expect(computedField.value).toEqual(NaN); + + expect(handleValidation({ field_a: { child: 10 } }).formErrors).toEqual(undefined); + expect(computedField.value).toEqual(100); + + expect(handleValidation({ field_a: { child: 11 } }).formErrors).toEqual(undefined); + expect(computedField.value).toEqual(110); + }); + + it.todo('Apply a conditional value in a nested field with a conditional extra validation.'); + + it.todo('From the top level I can reach into a nested fieldsets value for validations'); }); describe('Arrays', () => { diff --git a/src/tests/jsonLogicFixtures.js b/src/tests/jsonLogicFixtures.js index adf54dbe8..f6fa1c9da 100644 --- a/src/tests/jsonLogicFixtures.js +++ b/src/tests/jsonLogicFixtures.js @@ -689,6 +689,40 @@ export const twoLevelsOfJSONLogicSchema = { required: ['field_a', 'field_b'], }; +export const fieldsetWithComputedAttributes = { + properties: { + field_a: { + type: 'object', + 'x-jsf-presentation': { + inputType: 'fieldset', + }, + properties: { + child: { + type: 'number', + }, + other_child: { + type: 'number', + readOnly: true, + 'x-jsf-computedAttributes': { + value: 'child_times_10', + }, + }, + }, + required: ['child'], + 'x-jsf-logic': { + computedValues: { + child_times_10: { + rule: { + '*': [{ var: 'child' }, 10], + }, + }, + }, + }, + }, + }, + required: ['field_a'], +}; + export const schemaWithPropertyThatDoesNotExistInThatLevelButDoesInFieldset = { properties: { field_a: { From 7dd6eb477afc60110821d9af4c55060417e6148d Mon Sep 17 00:00:00 2001 From: brennj Date: Mon, 10 Jul 2023 12:16:28 +0200 Subject: [PATCH 44/69] chore: able to apply nested object validations --- src/calculateConditionalProperties.js | 4 +- src/createHeadlessForm.js | 2 +- src/helpers.js | 24 ++++-- src/jsonLogic.js | 14 +++- src/nodeProcessing/checkIfConditionMatches.js | 13 ++- src/tests/jsonLogic.test.js | 43 +++++++++- src/tests/jsonLogicFixtures.js | 80 +++++++++++++++++++ 7 files changed, 164 insertions(+), 16 deletions(-) diff --git a/src/calculateConditionalProperties.js b/src/calculateConditionalProperties.js index ec83d1f4f..3c231872f 100644 --- a/src/calculateConditionalProperties.js +++ b/src/calculateConditionalProperties.js @@ -69,7 +69,7 @@ function rebuildFieldset(fields, property) { * @param {FieldParameters} fieldParams - field parameters * @returns {Function} */ -export function calculateConditionalProperties(fieldParams, customProperties, validations) { +export function calculateConditionalProperties(fieldParams, customProperties, validations, config) { /** * Runs dynamic property calculation on a field based on a conditional that has been calculated * @param {Boolean} isRequired - if the field is required @@ -110,7 +110,7 @@ export function calculateConditionalProperties(fieldParams, customProperties, va fields: fieldSetFields, required: isRequired, }, - { config: { parentID: fieldParams.name } }, + config, validations ), }; diff --git a/src/createHeadlessForm.js b/src/createHeadlessForm.js index b31a9d72f..6422cfd69 100644 --- a/src/createHeadlessForm.js +++ b/src/createHeadlessForm.js @@ -240,7 +240,7 @@ function buildField(fieldParams, config, scopedJsonSchema, validations) { const yupSchema = buildYupSchema(fieldParams, config, validations); const calculateConditionalFieldsClosure = fieldParams.isDynamic && - calculateConditionalProperties(fieldParams, customProperties, validations); + calculateConditionalProperties(fieldParams, customProperties, validations, config); const calculateCustomValidationPropertiesClosure = calculateCustomValidationProperties( fieldParams, diff --git a/src/helpers.js b/src/helpers.js index 31be18a6d..f62b8660a 100644 --- a/src/helpers.js +++ b/src/helpers.js @@ -170,7 +170,7 @@ export function getPrefillValues(fields, initialValues = {}) { * @param {Object} node - JSON-schema node * @returns */ -function updateField(field, requiredFields, node, formValues, validations) { +function updateField(field, requiredFields, node, formValues, validations, config) { // If there was an error building the field, it might not exist in the form even though // it can be mentioned in the schema so we return early in that case if (!field) { @@ -219,7 +219,12 @@ function updateField(field, requiredFields, node, formValues, validations) { // If field has a calculateConditionalProperties closure, run it and update the field properties if (field.calculateConditionalProperties) { - const newFieldValues = field.calculateConditionalProperties(fieldIsRequired, node, validations); + const newFieldValues = field.calculateConditionalProperties( + fieldIsRequired, + node, + validations, + config + ); updateValues(newFieldValues); } @@ -263,6 +268,7 @@ export function processNode({ formValues, formFields, accRequired = new Set(), + parentID = 'root', validations, }) { // Set initial required fields @@ -271,13 +277,15 @@ export function processNode({ // Go through the node properties definition and update each field accordingly Object.keys(node.properties ?? []).forEach((fieldName) => { const field = getField(fieldName, formFields); - updateField(field, requiredFields, node, formValues, validations); + updateField(field, requiredFields, node, formValues, validations, { parentID }); }); // Update required fields based on the `required` property and mutate node if needed node.required?.forEach((fieldName) => { requiredFields.add(fieldName); - updateField(getField(fieldName, formFields), requiredFields, node, formValues, validations); + updateField(getField(fieldName, formFields), requiredFields, node, formValues, validations, { + parentID, + }); }); if (node.if) { @@ -290,6 +298,7 @@ export function processNode({ formValues, formFields, accRequired: requiredFields, + parentID, validations, }); @@ -300,6 +309,7 @@ export function processNode({ formValues, formFields, accRequired: requiredFields, + parentID, validations, }); branchRequired.forEach((field) => requiredFields.add(field)); @@ -315,7 +325,7 @@ export function processNode({ node.anyOf.forEach(({ required = [] }) => { required.forEach((fieldName) => { const field = getField(fieldName, formFields); - updateField(field, requiredFields, node, formValues, validations); + updateField(field, requiredFields, node, formValues, validations, { parentID }); }); }); } @@ -328,6 +338,7 @@ export function processNode({ formValues, formFields, accRequired: requiredFields, + parentID, validations, }) ) @@ -346,6 +357,7 @@ export function processNode({ formValues: formValues[name] || {}, formFields: getField(name, formFields).fields, validations, + parentID: name, }); } }); @@ -354,10 +366,10 @@ export function processNode({ if (node['x-jsf-logic']) { const { required: requiredFromLogic } = processJSONLogicNode({ node: node['x-jsf-logic'], - parentNode: node, formValues, formFields, accRequired: requiredFields, + parentID, validations, }); requiredFromLogic.forEach((field) => requiredFields.add(field)); diff --git a/src/jsonLogic.js b/src/jsonLogic.js index 89802dd1c..5b7d76d9f 100644 --- a/src/jsonLogic.js +++ b/src/jsonLogic.js @@ -137,13 +137,20 @@ export function calculateComputedAttributes(fieldParams, { parentID = 'root' } = }; } -export function processJSONLogicNode({ node, formFields, formValues, accRequired, validations }) { +export function processJSONLogicNode({ + node, + formFields, + formValues, + accRequired, + parentID, + validations, +}) { const requiredFields = new Set(accRequired); if (node.allOf) { node.allOf .map((allOfNode) => - processJSONLogicNode({ node: allOfNode, formValues, formFields, validations }) + processJSONLogicNode({ node: allOfNode, formValues, formFields, validations, parentID }) ) .forEach(({ required: allOfItemRequired }) => { allOfItemRequired.forEach(requiredFields.add, requiredFields); @@ -160,7 +167,8 @@ export function processJSONLogicNode({ node, formFields, formValues, accRequired const matchesValidationsAndComputedValues = checkIfMatchesValidationsAndComputedValues( node, formValues, - validations + validations, + parentID ); const isConditionMatch = matchesPropertyCondition && matchesValidationsAndComputedValues; diff --git a/src/nodeProcessing/checkIfConditionMatches.js b/src/nodeProcessing/checkIfConditionMatches.js index 4336aef85..58aee4814 100644 --- a/src/nodeProcessing/checkIfConditionMatches.js +++ b/src/nodeProcessing/checkIfConditionMatches.js @@ -62,16 +62,23 @@ export function checkIfConditionMatches(node, formValues, formFields, validation }); } -export function checkIfMatchesValidationsAndComputedValues(node, formValues, validations) { +export function checkIfMatchesValidationsAndComputedValues( + node, + formValues, + validations, + parentID +) { const validationsMatch = Object.entries(node.if.validations ?? {}).every(([name, property]) => { - const currentValue = validations.getScope().evaluateValidationRule(name, formValues); + const currentValue = validations.getScope(parentID).evaluateValidationRule(name, formValues); if (Object.hasOwn(property, 'const') && currentValue === property.const) return true; return false; }); const computedValuesMatch = Object.entries(node.if.computedValues ?? {}).every( ([name, property]) => { - const currentValue = validations.getScope().evaluateComputedValueRule(name, formValues); + const currentValue = validations + .getScope(parentID) + .evaluateComputedValueRule(name, formValues); if (Object.hasOwn(property, 'const') && currentValue === property.const) return true; return false; } diff --git a/src/tests/jsonLogic.test.js b/src/tests/jsonLogic.test.js index b93f61624..ed3e06444 100644 --- a/src/tests/jsonLogic.test.js +++ b/src/tests/jsonLogic.test.js @@ -3,6 +3,7 @@ import { createHeadlessForm } from '../createHeadlessForm'; import { createSchemaWithRulesOnFieldA, createSchemaWithThreePropertiesWithRuleOnFieldA, + fieldsetWithAConditionalToApplyExtraValidations, fieldsetWithComputedAttributes, multiRuleSchema, nestedFieldsetWithValidationSchema, @@ -368,6 +369,8 @@ describe('cross-value validations', () => { undefined ); }); + + it.todo('Should handle a enum as well as a const in the if statement?'); }); describe('Multiple validations', () => { @@ -411,6 +414,8 @@ describe('cross-value validations', () => { expect(fieldB.value).toEqual(4); expect(fieldB.label).toEqual('This is 4!'); }); + + it.todo('Should be able to reference values in errorMessages'); }); describe('Nested fieldsets', () => { @@ -478,7 +483,43 @@ describe('cross-value validations', () => { expect(computedField.value).toEqual(110); }); - it.todo('Apply a conditional value in a nested field with a conditional extra validation.'); + it('Apply a conditional value in a nested field with a conditional extra validation.', () => { + const { fields, handleValidation } = createHeadlessForm( + fieldsetWithAConditionalToApplyExtraValidations, + { + strictInputType: false, + } + ); + const [fieldA] = fields; + const [, , thirdChild] = fieldA.fields; + expect(thirdChild.isVisible).toEqual(false); + expect(thirdChild.required).toEqual(false); + + expect(handleValidation({ field_a: {} }).formErrors).toEqual({ + field_a: { child: 'Required field', other_child: 'Required field' }, + }); + expect(handleValidation({ field_a: { child: 0, other_child: 0 } }).formErrors).toEqual( + undefined + ); + expect(handleValidation({ field_a: { child: 10, other_child: 0 } }).formErrors).toEqual( + undefined + ); + expect(handleValidation({ field_a: { child: 10, other_child: 20 } }).formErrors).toEqual({ + field_a: { third_child: 'Required field' }, + }); + expect(thirdChild.isVisible).toEqual(true); + expect(thirdChild.required).toEqual(true); + + expect( + handleValidation({ field_a: { child: 10, other_child: 20, third_child: 10 } }).formErrors + ).toEqual({ + field_a: { third_child: 'Must be greater than other child.' }, + }); + + expect( + handleValidation({ field_a: { child: 10, other_child: 20, third_child: 30 } }).formErrors + ).toEqual(undefined); + }); it.todo('From the top level I can reach into a nested fieldsets value for validations'); }); diff --git a/src/tests/jsonLogicFixtures.js b/src/tests/jsonLogicFixtures.js index f6fa1c9da..6479863a4 100644 --- a/src/tests/jsonLogicFixtures.js +++ b/src/tests/jsonLogicFixtures.js @@ -723,6 +723,86 @@ export const fieldsetWithComputedAttributes = { required: ['field_a'], }; +export const fieldsetWithAConditionalToApplyExtraValidations = { + properties: { + field_a: { + type: 'object', + 'x-jsf-presentation': { + inputType: 'fieldset', + }, + properties: { + child: { + type: 'number', + }, + other_child: { + type: 'number', + }, + third_child: { + type: 'number', + }, + }, + required: ['child', 'other_child'], + 'x-jsf-logic': { + validations: { + child_is_greater_than_other_child: { + rule: { + '>': [{ var: 'child' }, { var: 'other_child' }], + }, + }, + third_child_is_greater_than_other_child: { + errorMessage: 'Must be greater than other child.', + rule: { + '>': [{ var: 'third_child' }, { var: 'other_child' }], + }, + }, + }, + computedValues: { + child_times_10: { + rule: { + '*': [{ var: 'child' }, 10], + }, + }, + }, + allOf: [ + { + if: { + computedValues: { + child_times_10: { + const: 100, + }, + }, + validations: { + child_is_greater_than_other_child: { + const: false, + }, + }, + properties: { + child: { + const: 10, + }, + }, + }, + then: { + required: ['third_child'], + properties: { + third_child: { + 'x-jsf-requiredValidations': ['third_child_is_greater_than_other_child'], + }, + }, + }, + else: { + properties: { + third_child: false, + }, + }, + }, + ], + }, + }, + }, + required: ['field_a'], +}; + export const schemaWithPropertyThatDoesNotExistInThatLevelButDoesInFieldset = { properties: { field_a: { From 30fbd6f501a17adfbd88ce71ea6aa75c24d86eb1 Mon Sep 17 00:00:00 2001 From: brennj Date: Mon, 10 Jul 2023 12:56:19 +0200 Subject: [PATCH 45/69] chore: more error handling --- src/jsonLogic.js | 27 +++++++++++++++------ src/tests/jsonLogic.test.js | 44 ++++++++++++++++++++++++++++------ src/tests/jsonLogicFixtures.js | 34 ++++++++++++++++++++++++++ 3 files changed, 91 insertions(+), 14 deletions(-) diff --git a/src/jsonLogic.js b/src/jsonLogic.js index 5b7d76d9f..c7c5cd67b 100644 --- a/src/jsonLogic.js +++ b/src/jsonLogic.js @@ -75,8 +75,13 @@ function createValidationsScope(schema) { const answer = jsonLogic.apply(validation.rule, clean(values)); return answer; }, - evaluateComputedValueRule(id, values) { + evaluateComputedValueRule(id, values, fieldName) { const validation = computedValuesMap.get(id); + if (validation === undefined) + throw Error( + `"${id}" computed property in field "${fieldName}" does not exist as a validation.` + ); + const answer = jsonLogic.apply(validation.rule, clean(values)); return answer; }, @@ -108,27 +113,35 @@ export function yupSchemaWithCustomJSONLogic({ field, validations, config, id }) ); } -function replaceHandlebarsTemplates(string, validations, formValues) { +function replaceHandlebarsTemplates(string, validations, formValues, parentID, fieldName) { return string.replace(/\{\{([^{}]+)\}\}/g, (match, key) => { - return validations.getScope().evaluateComputedValueRule(key.trim(), formValues); + return validations + .getScope(parentID) + .evaluateComputedValueRule(key.trim(), formValues, fieldName); }); } export function calculateComputedAttributes(fieldParams, { parentID = 'root' } = {}) { return ({ validations, formValues }) => { - const { computedAttributes } = fieldParams; + const { name, computedAttributes } = fieldParams; return Object.fromEntries( Object.entries(computedAttributes) .map(([key, value]) => { if (key === 'description') - return [key, replaceHandlebarsTemplates(value, validations, formValues)]; + return [ + key, + replaceHandlebarsTemplates(value, validations, formValues, parentID, name), + ]; if (key === 'title') { - return ['label', replaceHandlebarsTemplates(value, validations, formValues)]; + return [ + 'label', + replaceHandlebarsTemplates(value, validations, formValues, parentID, name), + ]; } if (key === 'const' || key === 'value') return [ key, - validations.getScope(parentID).evaluateComputedValueRule(value, formValues), + validations.getScope(parentID).evaluateComputedValueRule(value, formValues, name), ]; return [key, null]; }) diff --git a/src/tests/jsonLogic.test.js b/src/tests/jsonLogic.test.js index ed3e06444..683deb912 100644 --- a/src/tests/jsonLogic.test.js +++ b/src/tests/jsonLogic.test.js @@ -8,6 +8,9 @@ import { multiRuleSchema, nestedFieldsetWithValidationSchema, schemaWithChecksAndThenValidationsOnThen, + schemaWithComputedAttributeThatDoesntExist, + schemaWithComputedAttributeThatDoesntExistDescription, + schemaWithComputedAttributeThatDoesntExistTitle, schemaWithComputedAttributes, schemaWithComputedValueChecksInIf, schemaWithDeepVarThatDoesNotExist, @@ -118,11 +121,38 @@ describe('cross-value validations', () => { ); }); - it.todo('On x-jsf-computedAttributes, error if theres a value that does not exist.'); - it.todo( - 'On x-jsf-computedAttributes, error if theres a value that does not exist on a description.' - ); - it.todo('On x-jsf-computedAttributes, error if theres a value that does not exist on a title.'); + it('On x-jsf-computedAttributes, error if theres a value that does not exist.', () => { + createHeadlessForm(schemaWithComputedAttributeThatDoesntExist, { + strictInputType: false, + }); + expect(console.error).toHaveBeenCalledWith( + 'JSON Schema invalid!', + Error('"iDontExist" computed property in field "field_a" does not exist as a validation.') + ); + }); + + it('On x-jsf-computedAttributes, error if theres a value that does not exist on a title.', () => { + createHeadlessForm(schemaWithComputedAttributeThatDoesntExistTitle, { + strictInputType: false, + }); + expect(console.error).toHaveBeenCalledWith( + 'JSON Schema invalid!', + Error('"iDontExist" computed property in field "field_a" does not exist as a validation.') + ); + }); + + it('On x-jsf-computedAttributes, error if theres a value that does not exist on a description.', () => { + createHeadlessForm(schemaWithComputedAttributeThatDoesntExistDescription, { + strictInputType: false, + }); + expect(console.error).toHaveBeenCalledWith( + 'JSON Schema invalid!', + Error('"iDontExist" computed property in field "field_a" does not exist as a validation.') + ); + }); + + it.todo('Error for a missing computed value in an if'); + it.todo('Error for a missing validation in an if'); }); describe('Relative: <, >, =', () => { @@ -478,9 +508,11 @@ describe('cross-value validations', () => { expect(handleValidation({ field_a: { child: 10 } }).formErrors).toEqual(undefined); expect(computedField.value).toEqual(100); + expect(computedField.description).toEqual('this is 100'); expect(handleValidation({ field_a: { child: 11 } }).formErrors).toEqual(undefined); expect(computedField.value).toEqual(110); + expect(computedField.description).toEqual('this is 110'); }); it('Apply a conditional value in a nested field with a conditional extra validation.', () => { @@ -520,8 +552,6 @@ describe('cross-value validations', () => { handleValidation({ field_a: { child: 10, other_child: 20, third_child: 30 } }).formErrors ).toEqual(undefined); }); - - it.todo('From the top level I can reach into a nested fieldsets value for validations'); }); describe('Arrays', () => { diff --git a/src/tests/jsonLogicFixtures.js b/src/tests/jsonLogicFixtures.js index 6479863a4..08e1e7e77 100644 --- a/src/tests/jsonLogicFixtures.js +++ b/src/tests/jsonLogicFixtures.js @@ -162,6 +162,39 @@ export const schemaWithValidationThatDoesNotExistOnProperty = { }, }; +export const schemaWithComputedAttributeThatDoesntExist = { + properties: { + field_a: { + type: 'number', + 'x-jsf-computedAttributes': { + value: 'iDontExist', + }, + }, + }, +}; + +export const schemaWithComputedAttributeThatDoesntExistTitle = { + properties: { + field_a: { + type: 'number', + 'x-jsf-computedAttributes': { + title: `this doesn't exist {{iDontExist}}`, + }, + }, + }, +}; + +export const schemaWithComputedAttributeThatDoesntExistDescription = { + properties: { + field_a: { + type: 'number', + 'x-jsf-computedAttributes': { + description: `this doesn't exist {{iDontExist}}`, + }, + }, + }, +}; + export const schemaWithGreaterThanChecksForThreeFields = { properties: { field_a: { @@ -705,6 +738,7 @@ export const fieldsetWithComputedAttributes = { readOnly: true, 'x-jsf-computedAttributes': { value: 'child_times_10', + description: 'this is {{child_times_10}}', }, }, }, From 8e23efa2a5918044a1b5ef71414a90e26f47944a Mon Sep 17 00:00:00 2001 From: brennj Date: Mon, 10 Jul 2023 13:08:10 +0200 Subject: [PATCH 46/69] chore: MORE error handling --- src/jsonLogic.js | 23 +++++++++++++++---- src/nodeProcessing/checkIfConditionMatches.js | 2 +- src/tests/jsonLogic.test.js | 12 +++++++++- src/tests/jsonLogicFixtures.js | 21 +++++++++++++++++ 4 files changed, 51 insertions(+), 7 deletions(-) diff --git a/src/jsonLogic.js b/src/jsonLogic.js index c7c5cd67b..e185869d8 100644 --- a/src/jsonLogic.js +++ b/src/jsonLogic.js @@ -67,6 +67,11 @@ function createValidationsScope(schema) { computedValuesMap.set(id, computedValue); }); + function evaluateComputedValueRule(validation, values) { + const answer = jsonLogic.apply(validation.rule, clean(values)); + return answer; + } + return { validationMap, computedValuesMap, @@ -75,15 +80,21 @@ function createValidationsScope(schema) { const answer = jsonLogic.apply(validation.rule, clean(values)); return answer; }, - evaluateComputedValueRule(id, values, fieldName) { + evaluateComputedValueRuleForField(id, values, fieldName) { const validation = computedValuesMap.get(id); if (validation === undefined) throw Error( `"${id}" computed property in field "${fieldName}" does not exist as a validation.` ); - const answer = jsonLogic.apply(validation.rule, clean(values)); - return answer; + return evaluateComputedValueRule(validation, values); + }, + evaluateComputedValueRuleInCondition(id, values) { + const validation = computedValuesMap.get(id); + if (validation === undefined) + throw Error(`"${id}" computedValue in if condition doesn't exist.`); + + return evaluateComputedValueRule(validation, values); }, }; } @@ -117,7 +128,7 @@ function replaceHandlebarsTemplates(string, validations, formValues, parentID, f return string.replace(/\{\{([^{}]+)\}\}/g, (match, key) => { return validations .getScope(parentID) - .evaluateComputedValueRule(key.trim(), formValues, fieldName); + .evaluateComputedValueRuleForField(key.trim(), formValues, fieldName); }); } @@ -141,7 +152,9 @@ export function calculateComputedAttributes(fieldParams, { parentID = 'root' } = if (key === 'const' || key === 'value') return [ key, - validations.getScope(parentID).evaluateComputedValueRule(value, formValues, name), + validations + .getScope(parentID) + .evaluateComputedValueRuleForField(value, formValues, name), ]; return [key, null]; }) diff --git a/src/nodeProcessing/checkIfConditionMatches.js b/src/nodeProcessing/checkIfConditionMatches.js index 58aee4814..f0f9b28cd 100644 --- a/src/nodeProcessing/checkIfConditionMatches.js +++ b/src/nodeProcessing/checkIfConditionMatches.js @@ -78,7 +78,7 @@ export function checkIfMatchesValidationsAndComputedValues( ([name, property]) => { const currentValue = validations .getScope(parentID) - .evaluateComputedValueRule(name, formValues); + .evaluateComputedValueRuleInCondition(name, formValues); if (Object.hasOwn(property, 'const') && currentValue === property.const) return true; return false; } diff --git a/src/tests/jsonLogic.test.js b/src/tests/jsonLogic.test.js index 683deb912..d5ff11cab 100644 --- a/src/tests/jsonLogic.test.js +++ b/src/tests/jsonLogic.test.js @@ -5,6 +5,7 @@ import { createSchemaWithThreePropertiesWithRuleOnFieldA, fieldsetWithAConditionalToApplyExtraValidations, fieldsetWithComputedAttributes, + ifConditionWithMissingComputedValue, multiRuleSchema, nestedFieldsetWithValidationSchema, schemaWithChecksAndThenValidationsOnThen, @@ -151,7 +152,16 @@ describe('cross-value validations', () => { ); }); - it.todo('Error for a missing computed value in an if'); + it('Error for a missing computed value in an if', () => { + createHeadlessForm(ifConditionWithMissingComputedValue, { + strictInputType: false, + }); + expect(console.error).toHaveBeenCalledWith( + 'JSON Schema invalid!', + Error(`"iDontExist" computedValue in if condition doesn't exist.`) + ); + }); + it.todo('Error for a missing validation in an if'); }); diff --git a/src/tests/jsonLogicFixtures.js b/src/tests/jsonLogicFixtures.js index 08e1e7e77..1a53c73c6 100644 --- a/src/tests/jsonLogicFixtures.js +++ b/src/tests/jsonLogicFixtures.js @@ -195,6 +195,27 @@ export const schemaWithComputedAttributeThatDoesntExistDescription = { }, }; +export const ifConditionWithMissingComputedValue = { + properties: { + field_a: { + type: 'number', + }, + }, + 'x-jsf-logic': { + allOf: [ + { + if: { + computedValues: { + iDontExist: { + const: 10, + }, + }, + }, + }, + ], + }, +}; + export const schemaWithGreaterThanChecksForThreeFields = { properties: { field_a: { From 413e90570a236ed6c8f5bdf092a37d64f35c6792 Mon Sep 17 00:00:00 2001 From: brennj Date: Mon, 10 Jul 2023 14:05:50 +0200 Subject: [PATCH 47/69] chore: fix error string messaging --- src/jsonLogic.js | 4 +--- src/tests/jsonLogic.test.js | 6 +++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/jsonLogic.js b/src/jsonLogic.js index e185869d8..41ca78e0e 100644 --- a/src/jsonLogic.js +++ b/src/jsonLogic.js @@ -83,9 +83,7 @@ function createValidationsScope(schema) { evaluateComputedValueRuleForField(id, values, fieldName) { const validation = computedValuesMap.get(id); if (validation === undefined) - throw Error( - `"${id}" computed property in field "${fieldName}" does not exist as a validation.` - ); + throw Error(`"${id}" computedValue in field "${fieldName}" doesn't exist.`); return evaluateComputedValueRule(validation, values); }, diff --git a/src/tests/jsonLogic.test.js b/src/tests/jsonLogic.test.js index d5ff11cab..c3b3117a7 100644 --- a/src/tests/jsonLogic.test.js +++ b/src/tests/jsonLogic.test.js @@ -128,7 +128,7 @@ describe('cross-value validations', () => { }); expect(console.error).toHaveBeenCalledWith( 'JSON Schema invalid!', - Error('"iDontExist" computed property in field "field_a" does not exist as a validation.') + Error(`"iDontExist" computedValue in field "field_a" doesn't exist.`) ); }); @@ -138,7 +138,7 @@ describe('cross-value validations', () => { }); expect(console.error).toHaveBeenCalledWith( 'JSON Schema invalid!', - Error('"iDontExist" computed property in field "field_a" does not exist as a validation.') + Error(`"iDontExist" computedValue in field "field_a" doesn't exist.`) ); }); @@ -148,7 +148,7 @@ describe('cross-value validations', () => { }); expect(console.error).toHaveBeenCalledWith( 'JSON Schema invalid!', - Error('"iDontExist" computed property in field "field_a" does not exist as a validation.') + Error(`"iDontExist" computedValue in field "field_a" doesn't exist.`) ); }); From 36d433f61d1633a37ecfc51c4b4e3f4925932d48 Mon Sep 17 00:00:00 2001 From: brennj Date: Mon, 10 Jul 2023 14:10:44 +0200 Subject: [PATCH 48/69] chore: fail on missing validation in if --- src/jsonLogic.js | 17 +++++++++++---- src/nodeProcessing/checkIfConditionMatches.js | 4 +++- src/tests/jsonLogic.test.js | 11 +++++++++- src/tests/jsonLogicFixtures.js | 21 +++++++++++++++++++ 4 files changed, 47 insertions(+), 6 deletions(-) diff --git a/src/jsonLogic.js b/src/jsonLogic.js index 41ca78e0e..e2076c671 100644 --- a/src/jsonLogic.js +++ b/src/jsonLogic.js @@ -68,8 +68,11 @@ function createValidationsScope(schema) { }); function evaluateComputedValueRule(validation, values) { - const answer = jsonLogic.apply(validation.rule, clean(values)); - return answer; + return jsonLogic.apply(validation.rule, clean(values)); + } + + function evaluateValidation(validation, values) { + return jsonLogic.apply(validation.rule, clean(values)); } return { @@ -77,8 +80,14 @@ function createValidationsScope(schema) { computedValuesMap, evaluateValidationRule(id, values) { const validation = validationMap.get(id); - const answer = jsonLogic.apply(validation.rule, clean(values)); - return answer; + return evaluateValidation(validation, values); + }, + evaluateValidationRuleInCondition(id, values) { + const validation = validationMap.get(id); + if (validation === undefined) { + throw Error(`"${id}" validation in if condition doesn't exist.`); + } + return evaluateValidation(validation, values); }, evaluateComputedValueRuleForField(id, values, fieldName) { const validation = computedValuesMap.get(id); diff --git a/src/nodeProcessing/checkIfConditionMatches.js b/src/nodeProcessing/checkIfConditionMatches.js index f0f9b28cd..0b8924af2 100644 --- a/src/nodeProcessing/checkIfConditionMatches.js +++ b/src/nodeProcessing/checkIfConditionMatches.js @@ -69,7 +69,9 @@ export function checkIfMatchesValidationsAndComputedValues( parentID ) { const validationsMatch = Object.entries(node.if.validations ?? {}).every(([name, property]) => { - const currentValue = validations.getScope(parentID).evaluateValidationRule(name, formValues); + const currentValue = validations + .getScope(parentID) + .evaluateValidationRuleInCondition(name, formValues); if (Object.hasOwn(property, 'const') && currentValue === property.const) return true; return false; }); diff --git a/src/tests/jsonLogic.test.js b/src/tests/jsonLogic.test.js index c3b3117a7..876dba4e3 100644 --- a/src/tests/jsonLogic.test.js +++ b/src/tests/jsonLogic.test.js @@ -6,6 +6,7 @@ import { fieldsetWithAConditionalToApplyExtraValidations, fieldsetWithComputedAttributes, ifConditionWithMissingComputedValue, + ifConditionWithMissingValidation, multiRuleSchema, nestedFieldsetWithValidationSchema, schemaWithChecksAndThenValidationsOnThen, @@ -162,7 +163,15 @@ describe('cross-value validations', () => { ); }); - it.todo('Error for a missing validation in an if'); + it('Error for a missing validation in an if', () => { + createHeadlessForm(ifConditionWithMissingValidation, { + strictInputType: false, + }); + expect(console.error).toHaveBeenCalledWith( + 'JSON Schema invalid!', + Error(`"iDontExist" validation in if condition doesn't exist.`) + ); + }); }); describe('Relative: <, >, =', () => { diff --git a/src/tests/jsonLogicFixtures.js b/src/tests/jsonLogicFixtures.js index 1a53c73c6..3d3510abb 100644 --- a/src/tests/jsonLogicFixtures.js +++ b/src/tests/jsonLogicFixtures.js @@ -216,6 +216,27 @@ export const ifConditionWithMissingComputedValue = { }, }; +export const ifConditionWithMissingValidation = { + properties: { + field_a: { + type: 'number', + }, + }, + 'x-jsf-logic': { + allOf: [ + { + if: { + validations: { + iDontExist: { + const: true, + }, + }, + }, + }, + ], + }, +}; + export const schemaWithGreaterThanChecksForThreeFields = { properties: { field_a: { From dedd8f1fa4e26d93ef3284eea2756362d561b21e Mon Sep 17 00:00:00 2001 From: brennj Date: Mon, 10 Jul 2023 15:26:40 +0200 Subject: [PATCH 49/69] chore: start of support for array validation --- src/jsonLogic.js | 36 +++++++++++++++++------ src/tests/jsonLogic.test.js | 44 +++++++++++++++++++++++---- src/tests/jsonLogicFixtures.js | 54 ++++++++++++++++++++++++++++++++++ src/yupSchema.js | 6 +++- 4 files changed, 124 insertions(+), 16 deletions(-) diff --git a/src/jsonLogic.js b/src/jsonLogic.js index e2076c671..a1b72bcf7 100644 --- a/src/jsonLogic.js +++ b/src/jsonLogic.js @@ -18,8 +18,11 @@ export function createValidationChecker(schema) { function createScopes(jsonSchema, key = 'root') { scopes.set(key, createValidationsScope(jsonSchema)); Object.entries(jsonSchema?.properties ?? {}) - .filter(([, property]) => property.type === 'object') + .filter(([, property]) => property.type === 'object' || property.type === 'array') .forEach(([key, property]) => { + if (property.type === 'array') { + createScopes(property.items, `${key}[]`); + } createScopes(property, key); }); } @@ -232,13 +235,23 @@ export function processJSONLogicNode({ } function buildSampleEmptyObject(schema = {}) { - const { properties } = schema; - return Object.fromEntries( - Object.entries(properties ?? {}).map(([key, value]) => { - if (value.type !== 'object') return [key, true]; - return [key, buildSampleEmptyObject(value)]; - }) - ); + const sample = {}; + if (typeof schema !== 'object' || !schema.properties) { + return schema; + } + + for (const key in schema.properties) { + if (schema.properties[key].type === 'object') { + sample[key] = buildSampleEmptyObject(schema.properties[key]); + } else if (schema.properties[key].type === 'array') { + const itemSchema = schema.properties[key].items; + sample[key] = buildSampleEmptyObject(itemSchema); + } else { + sample[key] = true; + } + } + + return sample; } function checkRuleIntegrity(rule, id, data) { @@ -246,7 +259,7 @@ function checkRuleIntegrity(rule, id, data) { subRule.map((item) => { const isVar = item !== null && typeof item === 'object' && Object.hasOwn(item, 'var'); if (isVar) { - const exists = jsonLogic.apply({ var: item.var }, data); + const exists = jsonLogic.apply({ var: removeIndicesFromPath(item.var) }, data); if (exists === null) { throw Error(`"${item.var}" in rule "${id}" does not exist as a JSON schema property.`); } @@ -256,3 +269,8 @@ function checkRuleIntegrity(rule, id, data) { }); }); } + +function removeIndicesFromPath(path) { + const intermediatePath = path.replace(/\.\d+\./g, '.'); + return intermediatePath.replace(/\.\d+$/, ''); +} diff --git a/src/tests/jsonLogic.test.js b/src/tests/jsonLogic.test.js index 876dba4e3..8dbfe27d5 100644 --- a/src/tests/jsonLogic.test.js +++ b/src/tests/jsonLogic.test.js @@ -28,7 +28,9 @@ import { schemaWithTwoRules, schemaWithValidationThatDoesNotExistOnProperty, schemaWithVarThatDoesNotExist, + simpleArrayValidationSchema, twoLevelsOfJSONLogicSchema, + validatingASingleItemInTheArray, validatingTwoNestedFieldsSchema, } from './jsonLogicFixtures'; @@ -418,8 +420,6 @@ describe('cross-value validations', () => { undefined ); }); - - it.todo('Should handle a enum as well as a const in the if statement?'); }); describe('Multiple validations', () => { @@ -573,9 +573,41 @@ describe('cross-value validations', () => { }); }); - describe('Arrays', () => { - it.todo('How will this even work?'); - it.todo('What do I need to do when i need to validate all items'); - it.todo('What do i need to do when i need to validate a specific array item'); + describe('Array validation', () => { + it('Should apply the json logic on an individual array item', () => { + const { handleValidation } = createHeadlessForm(simpleArrayValidationSchema, { + strictInputType: false, + }); + expect(handleValidation({ field_array: [] }).formErrors).toEqual(undefined); + expect(handleValidation({ field_array: [{}] }).formErrors).toEqual({ + field_array: [{ array_item: 'Required field' }], + }); + expect(handleValidation({ field_array: [{ array_item: 1 }] }).formErrors).toEqual({ + field_array: [{ array_item: 'Must be divisible by two' }], + }); + expect(handleValidation({ field_array: [{ array_item: 2 }] }).formErrors).toEqual(undefined); + expect( + handleValidation({ field_array: [{ array_item: 2 }, { array_item: 1 }] }).formErrors + ).toEqual({ + field_array: [undefined, { array_item: 'Must be divisible by two' }], + }); + expect( + handleValidation({ field_array: [{ array_item: 2 }, { array_item: 2 }] }).formErrors + ).toEqual(undefined); + }); + + it('Validating a single item in an array should work', () => { + const { handleValidation } = createHeadlessForm(validatingASingleItemInTheArray, { + strictInputType: false, + }); + expect(handleValidation({ field_array: [] }).formErrors).toEqual(undefined); + expect(handleValidation({ field_array: [{ item: 0 }] }).formErrors).toEqual(undefined); + expect(handleValidation({ field_array: [{ item: 0 }, { item: 3 }] }).formErrors).toEqual({ + field_array: 'Second item in array must be divisible by 4', + }); + }); + + it.todo('Should be able to use conditionals in items'); + it.todo('A test where in each item, the number must be greater than the previous'); }); }); diff --git a/src/tests/jsonLogicFixtures.js b/src/tests/jsonLogicFixtures.js index 3d3510abb..57906a9dd 100644 --- a/src/tests/jsonLogicFixtures.js +++ b/src/tests/jsonLogicFixtures.js @@ -917,3 +917,57 @@ export const schemaWithPropertyThatDoesNotExistInThatLevelButDoesInFieldset = { }, required: ['field_a'], }; + +export const simpleArrayValidationSchema = { + properties: { + field_array: { + type: 'array', + items: { + properties: { + array_item: { + type: 'number', + 'x-jsf-requiredValidations': ['divisible_by_two'], + }, + }, + required: ['array_item'], + 'x-jsf-logic': { + validations: { + divisible_by_two: { + errorMessage: 'Must be divisible by two', + rule: { + '===': [{ '%': [{ var: 'array_item' }, 2] }, 0], + }, + }, + }, + }, + }, + }, + }, +}; + +export const validatingASingleItemInTheArray = { + properties: { + field_array: { + type: 'array', + 'x-jsf-requiredValidations': ['second_item_is_divisible_by_four'], + items: { + properties: { + item: { + type: 'number', + }, + }, + required: ['item'], + }, + }, + }, + 'x-jsf-logic': { + validations: { + second_item_is_divisible_by_four: { + errorMessage: 'Second item in array must be divisible by 4', + rule: { + '===': [{ '%': [{ var: 'field_array.1.item' }, 4] }, 0], + }, + }, + }, + }, +}; diff --git a/src/yupSchema.js b/src/yupSchema.js index 095b2b512..20d52c1d2 100644 --- a/src/yupSchema.js +++ b/src/yupSchema.js @@ -285,7 +285,11 @@ export function buildYupSchema(field, config, validations) { propertyFields.nthFieldGroup.fields().reduce( (schema, groupArrayField) => ({ ...schema, - [groupArrayField.name]: buildYupSchema(groupArrayField, config, validations)(), + [groupArrayField.name]: buildYupSchema( + groupArrayField, + { ...config, parentID: `${propertyFields.nthFieldGroup.name}[]` }, + validations + )(), }), {} ) From b738856abe23704a583145414a89062c4b088ecc Mon Sep 17 00:00:00 2001 From: brennj Date: Mon, 10 Jul 2023 16:35:56 +0200 Subject: [PATCH 50/69] chore: blocked fixing this test for now --- src/tests/jsonLogic.test.js | 2 +- src/tests/jsonLogicFixtures.js | 45 ++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/src/tests/jsonLogic.test.js b/src/tests/jsonLogic.test.js index 8dbfe27d5..33a576d97 100644 --- a/src/tests/jsonLogic.test.js +++ b/src/tests/jsonLogic.test.js @@ -607,7 +607,7 @@ describe('cross-value validations', () => { }); }); + // FIXME: This doesn't work because conditionals in items are not supported. it.todo('Should be able to use conditionals in items'); - it.todo('A test where in each item, the number must be greater than the previous'); }); }); diff --git a/src/tests/jsonLogicFixtures.js b/src/tests/jsonLogicFixtures.js index 57906a9dd..3436def6f 100644 --- a/src/tests/jsonLogicFixtures.js +++ b/src/tests/jsonLogicFixtures.js @@ -971,3 +971,48 @@ export const validatingASingleItemInTheArray = { }, }, }; + +// FIXME: This doesn't work because conditionals in items are not supported. +export const conditionalAppliedInAnItem = { + properties: { + field_array: { + type: 'array', + items: { + properties: { + item: { + type: 'number', + }, + other_item: { + type: 'number', + }, + }, + required: ['item'], + 'x-jsf-logic': { + validations: { + divisible_by_three: { + rule: { + '===': [{ '%': [{ var: 'item' }, 3] }, 0], + }, + }, + other_item_divisible_by_three: { + errorMessage: 'Must be disivisble_by_three', + rule: { + '===': [{ '%': [{ var: 'other_item' }, 3] }, 0], + }, + }, + }, + allOf: [ + { + if: { validations: { divisible_by_three: { cosnt: true } } }, + then: { + required: ['other_item'], + other_item: { 'x-jsf-requiredValidations': ['other_item_divisible_by_three'] }, + }, + else: { properties: { other_item: false } }, + }, + ], + }, + }, + }, + }, +}; From 7fd365046dbda7ead48efeb01b628de2f8539cb2 Mon Sep 17 00:00:00 2001 From: brennj Date: Mon, 10 Jul 2023 16:41:54 +0200 Subject: [PATCH 51/69] chore: errorMessage ref is out of scope for now --- src/tests/jsonLogic.test.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/tests/jsonLogic.test.js b/src/tests/jsonLogic.test.js index 33a576d97..4bed918da 100644 --- a/src/tests/jsonLogic.test.js +++ b/src/tests/jsonLogic.test.js @@ -463,8 +463,6 @@ describe('cross-value validations', () => { expect(fieldB.value).toEqual(4); expect(fieldB.label).toEqual('This is 4!'); }); - - it.todo('Should be able to reference values in errorMessages'); }); describe('Nested fieldsets', () => { From 8327b0784aff2bbad874623bc6dc583532e4a21b Mon Sep 17 00:00:00 2001 From: brennj Date: Thu, 13 Jul 2023 09:19:43 +0200 Subject: [PATCH 52/69] chore: some missing tests to do --- src/jsonLogic.js | 1 + src/tests/jsonLogic.test.js | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/src/jsonLogic.js b/src/jsonLogic.js index a1b72bcf7..2ae2a89d3 100644 --- a/src/jsonLogic.js +++ b/src/jsonLogic.js @@ -256,6 +256,7 @@ function buildSampleEmptyObject(schema = {}) { function checkRuleIntegrity(rule, id, data) { Object.values(rule ?? {}).map((subRule) => { + if (!Array.isArray(subRule) && subRule !== null && subRule !== undefined) return; subRule.map((item) => { const isVar = item !== null && typeof item === 'object' && Object.hasOwn(item, 'var'); if (isVar) { diff --git a/src/tests/jsonLogic.test.js b/src/tests/jsonLogic.test.js index 4bed918da..89ec593bc 100644 --- a/src/tests/jsonLogic.test.js +++ b/src/tests/jsonLogic.test.js @@ -420,6 +420,11 @@ describe('cross-value validations', () => { undefined ); }); + + it.todo('Apply validations and computed values on normal if statement.'); + it.todo( + 'When we have a required validation on a top level property and another validation is added, both should be accounted for.' + ); }); describe('Multiple validations', () => { From 41f6d1edf229cffd1023b292e0ba5d93a7aa0ac1 Mon Sep 17 00:00:00 2001 From: brennj Date: Thu, 13 Jul 2023 11:18:53 +0200 Subject: [PATCH 53/69] chore: conditional computedAttributes didnt work --- src/calculateConditionalProperties.js | 11 ++++- src/helpers.js | 3 +- src/jsonLogic.js | 59 +++++++++++++++++---------- src/tests/jsonLogic.test.js | 12 ++++++ src/tests/jsonLogicFixtures.js | 37 +++++++++++++++++ 5 files changed, 98 insertions(+), 24 deletions(-) diff --git a/src/calculateConditionalProperties.js b/src/calculateConditionalProperties.js index 3c231872f..bf407912f 100644 --- a/src/calculateConditionalProperties.js +++ b/src/calculateConditionalProperties.js @@ -4,6 +4,7 @@ import omit from 'lodash/omit'; import { extractParametersFromNode } from './helpers'; import { supportedTypes } from './internals/fields'; import { getFieldDescription, pickXKey } from './internals/helpers'; +import { calculateComputedAttributes } from './jsonLogic'; import { buildYupSchema } from './yupSchema'; /** * @typedef {import('./createHeadlessForm').FieldParameters} FieldParameters @@ -76,7 +77,7 @@ export function calculateConditionalProperties(fieldParams, customProperties, va * @param {Object} conditionBranch - condition branch being applied * @returns {Object} updated field parameters */ - return (isRequired, conditionBranch) => { + return (isRequired, conditionBranch, __, _, formValues) => { // Check if the current field is conditionally declared in the schema const conditionalProperty = conditionBranch?.properties?.[fieldParams.name]; @@ -98,6 +99,11 @@ export function calculateConditionalProperties(fieldParams, customProperties, va newFieldParams.fields = fieldSetFields; } + const { computedAttributes, ...restNewFieldParams } = newFieldParams; + const caclulatedComputedAttributes = computedAttributes + ? calculateComputedAttributes(newFieldParams, config)({ validations, formValues }) + : {}; + const base = { isVisible: true, required: isRequired, @@ -105,7 +111,8 @@ export function calculateConditionalProperties(fieldParams, customProperties, va schema: buildYupSchema( { ...fieldParams, - ...newFieldParams, + ...restNewFieldParams, + ...caclulatedComputedAttributes, // If there are inner fields (case of fieldset) they need to be updated based on the condition fields: fieldSetFields, required: isRequired, diff --git a/src/helpers.js b/src/helpers.js index f62b8660a..d630c0060 100644 --- a/src/helpers.js +++ b/src/helpers.js @@ -223,7 +223,8 @@ function updateField(field, requiredFields, node, formValues, validations, confi fieldIsRequired, node, validations, - config + config, + formValues ); updateValues(newFieldValues); } diff --git a/src/jsonLogic.js b/src/jsonLogic.js index 2ae2a89d3..9ba2d26c6 100644 --- a/src/jsonLogic.js +++ b/src/jsonLogic.js @@ -147,32 +147,49 @@ export function calculateComputedAttributes(fieldParams, { parentID = 'root' } = const { name, computedAttributes } = fieldParams; return Object.fromEntries( Object.entries(computedAttributes) - .map(([key, value]) => { - if (key === 'description') - return [ - key, - replaceHandlebarsTemplates(value, validations, formValues, parentID, name), - ]; - if (key === 'title') { - return [ - 'label', - replaceHandlebarsTemplates(value, validations, formValues, parentID, name), - ]; - } - if (key === 'const' || key === 'value') - return [ - key, - validations - .getScope(parentID) - .evaluateComputedValueRuleForField(value, formValues, name), - ]; - return [key, null]; - }) + .map(handleComputedAttribute(validations, formValues, parentID, name)) .filter(([, value]) => value !== null) ); }; } +function handleComputedAttribute(validations, formValues, parentID, name) { + return ([key, value]) => { + if (key === 'description') + return [key, replaceHandlebarsTemplates(value, validations, formValues, parentID, name)]; + + if (key === 'title') { + return ['label', replaceHandlebarsTemplates(value, validations, formValues, parentID, name)]; + } + + if (key === 'const' || key === 'value') + return [ + key, + validations.getScope(parentID).evaluateComputedValueRuleForField(value, formValues, name), + ]; + + if (key === 'x-jsf-errorMessage') { + return [ + 'errorMessage', + handleComputedErrorMessages(value, formValues, parentID, validations, name), + ]; + } + + return [ + key, + validations.getScope(parentID).evaluateComputedValueRuleForField(value, formValues, name), + ]; + }; +} + +function handleComputedErrorMessages(values, formValues, parentID, validations, name) { + return Object.fromEntries( + Object.entries(values).map(([key, value]) => { + return [key, replaceHandlebarsTemplates(value, validations, formValues, parentID, name)]; + }) + ); +} + export function processJSONLogicNode({ node, formFields, diff --git a/src/tests/jsonLogic.test.js b/src/tests/jsonLogic.test.js index 89ec593bc..313a42728 100644 --- a/src/tests/jsonLogic.test.js +++ b/src/tests/jsonLogic.test.js @@ -1,6 +1,7 @@ import { createHeadlessForm } from '../createHeadlessForm'; import { + aConditionallyAppliedComputedAttribute, createSchemaWithRulesOnFieldA, createSchemaWithThreePropertiesWithRuleOnFieldA, fieldsetWithAConditionalToApplyExtraValidations, @@ -468,6 +469,17 @@ describe('cross-value validations', () => { expect(fieldB.value).toEqual(4); expect(fieldB.label).toEqual('This is 4!'); }); + + it('computedAttribute test that minimum, errorMessages.minimum is working', () => { + const { handleValidation } = createHeadlessForm(aConditionallyAppliedComputedAttribute, { + strictInputType: false, + }); + expect(handleValidation({ field_a: 20, field_b: 1 }).formErrors).toEqual({ + field_b: 'use 10 or more', + }); + }); + + it.todo('computedAttribute test that maximum, errorMessages.maximum is working'); }); describe('Nested fieldsets', () => { diff --git a/src/tests/jsonLogicFixtures.js b/src/tests/jsonLogicFixtures.js index 3436def6f..414b972ef 100644 --- a/src/tests/jsonLogicFixtures.js +++ b/src/tests/jsonLogicFixtures.js @@ -1016,3 +1016,40 @@ export const conditionalAppliedInAnItem = { }, }, }; + +export const aConditionallyAppliedComputedAttribute = { + properties: { + field_a: { + type: 'number', + }, + field_b: { + type: 'number', + }, + }, + allOf: [ + { + if: { properties: { field_a: { const: 20 } } }, + then: { + properties: { + field_b: { + 'x-jsf-computedAttributes': { + minimum: 'a_divided_by_two', + 'x-jsf-errorMessage': { + minimum: 'use {{a_divided_by_two}} or more', + }, + }, + }, + }, + }, + }, + ], + 'x-jsf-logic': { + computedValues: { + a_divided_by_two: { + rule: { + '/': [{ var: 'field_a' }, 2], + }, + }, + }, + }, +}; From 5632f3508547217cdd8db403af49625915e31d6f Mon Sep 17 00:00:00 2001 From: brennj Date: Thu, 13 Jul 2023 11:58:18 +0200 Subject: [PATCH 54/69] chore: calculated value conditionally wasnt working --- src/calculateConditionalProperties.js | 3 ++ src/tests/jsonLogic.test.js | 29 ++++++++++++++--- src/tests/jsonLogicFixtures.js | 45 ++++++++++++++++++++++++++- 3 files changed, 72 insertions(+), 5 deletions(-) diff --git a/src/calculateConditionalProperties.js b/src/calculateConditionalProperties.js index bf407912f..12872a3a8 100644 --- a/src/calculateConditionalProperties.js +++ b/src/calculateConditionalProperties.js @@ -108,6 +108,9 @@ export function calculateConditionalProperties(fieldParams, customProperties, va isVisible: true, required: isRequired, ...(presentation?.inputType && { type: presentation.inputType }), + ...(caclulatedComputedAttributes.value + ? { value: caclulatedComputedAttributes.value } + : { value: undefined }), schema: buildYupSchema( { ...fieldParams, diff --git a/src/tests/jsonLogic.test.js b/src/tests/jsonLogic.test.js index 313a42728..6e4553881 100644 --- a/src/tests/jsonLogic.test.js +++ b/src/tests/jsonLogic.test.js @@ -1,7 +1,8 @@ import { createHeadlessForm } from '../createHeadlessForm'; import { - aConditionallyAppliedComputedAttribute, + aConditionallyAppliedComputedAttributeMinimum, + aConditionallyAppliedComputedAttributeValue, createSchemaWithRulesOnFieldA, createSchemaWithThreePropertiesWithRuleOnFieldA, fieldsetWithAConditionalToApplyExtraValidations, @@ -471,15 +472,35 @@ describe('cross-value validations', () => { }); it('computedAttribute test that minimum, errorMessages.minimum is working', () => { - const { handleValidation } = createHeadlessForm(aConditionallyAppliedComputedAttribute, { - strictInputType: false, - }); + const { handleValidation } = createHeadlessForm( + aConditionallyAppliedComputedAttributeMinimum, + { + strictInputType: false, + } + ); expect(handleValidation({ field_a: 20, field_b: 1 }).formErrors).toEqual({ field_b: 'use 10 or more', }); }); it.todo('computedAttribute test that maximum, errorMessages.maximum is working'); + + it('Apply a conditional computed Attrbute value', () => { + const { fields, handleValidation } = createHeadlessForm( + aConditionallyAppliedComputedAttributeValue, + { + strictInputType: false, + } + ); + + //TODO: this should fail because we have a const: 10, it should have an error from yup saying only 10 is accepted. + expect(handleValidation({ field_a: 20, field_b: 1 }).formErrors).toEqual(); + + const [, fieldB] = fields; + expect(fieldB.value).toEqual(10); + expect(handleValidation({ field_a: 10, field_b: 1 }).formErrors).toEqual(); + expect(fieldB.value).toEqual(undefined); + }); }); describe('Nested fieldsets', () => { diff --git a/src/tests/jsonLogicFixtures.js b/src/tests/jsonLogicFixtures.js index 414b972ef..6b9d458c1 100644 --- a/src/tests/jsonLogicFixtures.js +++ b/src/tests/jsonLogicFixtures.js @@ -1017,7 +1017,7 @@ export const conditionalAppliedInAnItem = { }, }; -export const aConditionallyAppliedComputedAttribute = { +export const aConditionallyAppliedComputedAttributeMinimum = { properties: { field_a: { type: 'number', @@ -1053,3 +1053,46 @@ export const aConditionallyAppliedComputedAttribute = { }, }, }; + +export const aConditionallyAppliedComputedAttributeValue = { + properties: { + field_a: { + type: 'number', + }, + field_b: { + type: 'number', + }, + }, + allOf: [ + { + if: { properties: { field_a: { const: 20 } } }, + then: { + properties: { + field_b: { + readOnly: true, + 'x-jsf-computedAttributes': { + const: 'a_divided_by_two', + value: 'a_divided_by_two', + }, + }, + }, + }, + else: { + properties: { + field_b: { + readOnly: false, + }, + }, + }, + }, + ], + 'x-jsf-logic': { + computedValues: { + a_divided_by_two: { + rule: { + '/': [{ var: 'field_a' }, 2], + }, + }, + }, + }, +}; From 893f6e7863d99616addd1bdcd23c2a65c190c283 Mon Sep 17 00:00:00 2001 From: brennj Date: Wed, 19 Jul 2023 16:29:09 +0200 Subject: [PATCH 55/69] chore: some statement handling --- src/calculateConditionalProperties.js | 1 + src/jsonLogic.js | 13 +++++++++++++ 2 files changed, 14 insertions(+) diff --git a/src/calculateConditionalProperties.js b/src/calculateConditionalProperties.js index 12872a3a8..074de44ac 100644 --- a/src/calculateConditionalProperties.js +++ b/src/calculateConditionalProperties.js @@ -108,6 +108,7 @@ export function calculateConditionalProperties(fieldParams, customProperties, va isVisible: true, required: isRequired, ...(presentation?.inputType && { type: presentation.inputType }), + ...caclulatedComputedAttributes, ...(caclulatedComputedAttributes.value ? { value: caclulatedComputedAttributes.value } : { value: undefined }), diff --git a/src/jsonLogic.js b/src/jsonLogic.js index 9ba2d26c6..914aeaf04 100644 --- a/src/jsonLogic.js +++ b/src/jsonLogic.js @@ -6,6 +6,12 @@ import { checkIfMatchesValidationsAndComputedValues, } from './nodeProcessing/checkIfConditionMatches'; +jsonLogic.add_operation('Number.toFixed', (a, b) => { + if (typeof a === 'number') return a.toFixed(b); + else if (typeof a === 'string') return parseFloat(a).toFixed(b); + else return a; +}); + /** * Parses the JSON schema to extract the advanced validation logic and returns a set of functionality to check the current status of said rules. * @param {Object} schema - JSON schema node @@ -175,6 +181,13 @@ function handleComputedAttribute(validations, formValues, parentID, name) { ]; } + if (key === 'x-jsf-presentation' && value.statement) { + return [ + 'statement', + handleComputedErrorMessages(value.statement, formValues, parentID, validations, name), + ]; + } + return [ key, validations.getScope(parentID).evaluateComputedValueRuleForField(value, formValues, name), From 5baaff93b1b0f79962c9d86a613fa413a556f578 Mon Sep 17 00:00:00 2001 From: brennj Date: Fri, 28 Jul 2023 12:18:56 +0200 Subject: [PATCH 56/69] feat: exploring inline rules --- src/jsonLogic.js | 51 ++++++++++++++-------- src/tests/jsonLogic.test.js | 23 ++++++++++ src/tests/jsonLogicFixtures.js | 80 ++++++++++++++++++++++++++++++++++ 3 files changed, 135 insertions(+), 19 deletions(-) diff --git a/src/jsonLogic.js b/src/jsonLogic.js index 914aeaf04..d3ccaace4 100644 --- a/src/jsonLogic.js +++ b/src/jsonLogic.js @@ -76,41 +76,38 @@ function createValidationsScope(schema) { computedValuesMap.set(id, computedValue); }); - function evaluateComputedValueRule(validation, values) { - return jsonLogic.apply(validation.rule, clean(values)); - } - - function evaluateValidation(validation, values) { - return jsonLogic.apply(validation.rule, clean(values)); + function evaluateValidation(rule, values) { + return jsonLogic.apply(rule, clean(values)); } return { validationMap, computedValuesMap, + evaluateValidation, evaluateValidationRule(id, values) { const validation = validationMap.get(id); - return evaluateValidation(validation, values); + return evaluateValidation(validation.rule, values); }, evaluateValidationRuleInCondition(id, values) { const validation = validationMap.get(id); if (validation === undefined) { throw Error(`"${id}" validation in if condition doesn't exist.`); } - return evaluateValidation(validation, values); + return evaluateValidation(validation.rule, values); }, evaluateComputedValueRuleForField(id, values, fieldName) { const validation = computedValuesMap.get(id); if (validation === undefined) throw Error(`"${id}" computedValue in field "${fieldName}" doesn't exist.`); - return evaluateComputedValueRule(validation, values); + return evaluateValidation(validation.rule, values); }, evaluateComputedValueRuleInCondition(id, values) { const validation = computedValuesMap.get(id); if (validation === undefined) throw Error(`"${id}" computedValue in if condition doesn't exist.`); - return evaluateComputedValueRule(validation, values); + return evaluateValidation(validation.rule, values); }, }; } @@ -140,12 +137,25 @@ export function yupSchemaWithCustomJSONLogic({ field, validations, config, id }) ); } -function replaceHandlebarsTemplates(string, validations, formValues, parentID, fieldName) { - return string.replace(/\{\{([^{}]+)\}\}/g, (match, key) => { - return validations - .getScope(parentID) - .evaluateComputedValueRuleForField(key.trim(), formValues, fieldName); - }); +function replaceHandlebarsTemplates({ + value: toReplace, + validations, + formValues, + parentID, + name: fieldName, +}) { + if (typeof toReplace === 'string') { + return toReplace.replace(/\{\{([^{}]+)\}\}/g, (match, key) => { + return validations + .getScope(parentID) + .evaluateComputedValueRuleForField(key.trim(), formValues, fieldName); + }); + } else if (typeof toReplace === 'object') { + const { rule, value } = toReplace; + const computedInlineValue = validations.getScope(parentID).evaluateValidation(rule, formValues); + if (value) return value.replaceAll('{{rule}}', computedInlineValue); + else return computedInlineValue; + } } export function calculateComputedAttributes(fieldParams, { parentID = 'root' } = {}) { @@ -162,10 +172,13 @@ export function calculateComputedAttributes(fieldParams, { parentID = 'root' } = function handleComputedAttribute(validations, formValues, parentID, name) { return ([key, value]) => { if (key === 'description') - return [key, replaceHandlebarsTemplates(value, validations, formValues, parentID, name)]; + return [key, replaceHandlebarsTemplates({ value, validations, formValues, parentID, name })]; if (key === 'title') { - return ['label', replaceHandlebarsTemplates(value, validations, formValues, parentID, name)]; + return [ + 'label', + replaceHandlebarsTemplates({ value, validations, formValues, parentID, name }), + ]; } if (key === 'const' || key === 'value') @@ -198,7 +211,7 @@ function handleComputedAttribute(validations, formValues, parentID, name) { function handleComputedErrorMessages(values, formValues, parentID, validations, name) { return Object.fromEntries( Object.entries(values).map(([key, value]) => { - return [key, replaceHandlebarsTemplates(value, validations, formValues, parentID, name)]; + return [key, replaceHandlebarsTemplates({ value, validations, formValues, parentID, name })]; }) ); } diff --git a/src/tests/jsonLogic.test.js b/src/tests/jsonLogic.test.js index 6e4553881..3389b4284 100644 --- a/src/tests/jsonLogic.test.js +++ b/src/tests/jsonLogic.test.js @@ -21,6 +21,7 @@ import { schemaWithDeepVarThatDoesNotExistOnFieldset, schemaWithGreaterThanChecksForThreeFields, schemaWithIfStatementWithComputedValuesAndValidationChecks, + schemaWithInlineRuleForComputedAttributeWithCopy, schemaWithMissingRule, schemaWithMultipleComputedValueChecks, schemaWithNativeAndJSONLogicChecks, @@ -176,6 +177,10 @@ describe('cross-value validations', () => { Error(`"iDontExist" validation in if condition doesn't exist.`) ); }); + + it.todo( + 'On an inline rule for a computedAttribute, error if theres a value referenced that does not exist' + ); }); describe('Relative: <, >, =', () => { @@ -501,6 +506,24 @@ describe('cross-value validations', () => { expect(handleValidation({ field_a: 10, field_b: 1 }).formErrors).toEqual(); expect(fieldB.value).toEqual(undefined); }); + + it('Use a self contained rule in a schema for a title attribute', () => { + const { fields, handleValidation } = createHeadlessForm( + schemaWithInlineRuleForComputedAttributeWithCopy, + { + strictInputType: false, + } + ); + const [, fieldB] = fields; + expect(handleValidation({ field_a: 0 }).formErrors).toEqual(undefined); + expect(fieldB.label).toEqual('I need this to work using the 10.'); + expect(handleValidation({ field_a: 10 }).formErrors).toEqual(undefined); + expect(fieldB.label).toEqual('I need this to work using the 20.'); + }); + + it.todo('Use a self contained rule in a schema for a title but it just uses the value'); + it.todo('Use a self contained rule for a minimum value'); + it.todo('Use a self contained rule for a conditionally applied schema'); }); describe('Nested fieldsets', () => { diff --git a/src/tests/jsonLogicFixtures.js b/src/tests/jsonLogicFixtures.js index 6b9d458c1..39d2916c9 100644 --- a/src/tests/jsonLogicFixtures.js +++ b/src/tests/jsonLogicFixtures.js @@ -173,6 +173,21 @@ export const schemaWithComputedAttributeThatDoesntExist = { }, }; +export const schemaWithInlinedRuleOnComputedAttributeThatReferencesUnknownVar = { + properties: { + field_a: { + type: 'number', + 'x-jsf-computedAttributes': { + title: { + rule: { + '+': [{ var: 'IdontExist' }], + }, + }, + }, + }, + }, +}; + export const schemaWithComputedAttributeThatDoesntExistTitle = { properties: { field_a: { @@ -1096,3 +1111,68 @@ export const aConditionallyAppliedComputedAttributeValue = { }, }, }; + +export const schemaWithInlineRuleForComputedAttributeWithCopy = { + properties: { + field_a: { + type: 'number', + }, + field_b: { + type: 'number', + 'x-jsf-computedAttributes': { + title: { + value: 'I need this to work using the {{rule}}.', + rule: { + '+': [{ var: 'field_a' }, 10], + }, + }, + }, + }, + }, +}; + +export const schemaWithInlineRuleForComputedAttributeWithoutCopy = { + properties: { + field_a: { + type: 'number', + }, + field_b: { + type: 'number', + 'x-jsf-computedAttributes': { + title: { + rule: { + '+': [{ var: 'field_a' }, 10], + }, + }, + }, + }, + }, +}; + +export const schemaWithInlineRuleForComputedAttributeWithOnlyTheRule = { + properties: { + field_a: { + type: 'number', + }, + field_b: { + type: 'number', + 'x-jsf-computedAttributes': { + minumum: { + rule: { + '+': [{ var: 'field_a' }, 10], + }, + }, + 'x-jsf-errorMessage': { + minimum: { + value: 'This should be greater than {{rule}}.', + rule: { + '+': [{ var: 'field_a' }, 10], + }, + }, + }, + }, + }, + }, +}; + +export const schemaWithInlineRuleForComputedAttributeInConditionallyAppliedSchema = {}; From e012445c7915d45f6fc7042856eb683fc603e8d3 Mon Sep 17 00:00:00 2001 From: brennj Date: Wed, 9 Aug 2023 14:26:23 +0200 Subject: [PATCH 57/69] chore: fix broken tests --- src/jsonLogic.js | 31 +++++++++++++++++++++++++++++-- src/tests/jsonLogic.test.js | 17 +++++++++++++---- 2 files changed, 42 insertions(+), 6 deletions(-) diff --git a/src/jsonLogic.js b/src/jsonLogic.js index fc5491d7a..093a0f9e0 100644 --- a/src/jsonLogic.js +++ b/src/jsonLogic.js @@ -22,6 +22,7 @@ export function createValidationChecker(schema) { const scopes = new Map(); function createScopes(jsonSchema, key = 'root') { + const sampleEmptyObject = buildSampleEmptyObject(schema); scopes.set(key, createValidationsScope(jsonSchema)); Object.entries(jsonSchema?.properties ?? {}) .filter(([, property]) => property.type === 'object' || property.type === 'array') @@ -31,6 +32,8 @@ export function createValidationChecker(schema) { } createScopes(property, key); }); + + validateInlineRules(jsonSchema, sampleEmptyObject); } createScopes(schema); @@ -297,7 +300,31 @@ function buildSampleEmptyObject(schema = {}) { return sample; } -function checkRuleIntegrity(rule, id, data) { +function validateInlineRules(jsonSchema, sampleEmptyObject) { + const properties = (jsonSchema?.properties || jsonSchema?.items?.properties) ?? {}; + Object.entries(properties) + .filter(([, property]) => property['x-jsf-computedAttributes'] !== undefined) + .forEach(([fieldName, property]) => { + Object.entries(property['x-jsf-computedAttributes']) + .filter(([, value]) => typeof value === 'object') + .forEach(([key, { rule }]) => { + checkRuleIntegrity( + rule, + fieldName, + sampleEmptyObject, + (item) => + `"${item.var}" in inline rule in property "${fieldName}.x-jsf-computedAttributes.${key}" does not exist as a JSON schema property.` + ); + }); + }); +} + +function checkRuleIntegrity( + rule, + id, + data, + errorMessage = (item) => `"${item.var}" in rule "${id}" does not exist as a JSON schema property.` +) { Object.values(rule ?? {}).map((subRule) => { if (!Array.isArray(subRule) && subRule !== null && subRule !== undefined) return; subRule.map((item) => { @@ -305,7 +332,7 @@ function checkRuleIntegrity(rule, id, data) { if (isVar) { const exists = jsonLogic.apply({ var: removeIndicesFromPath(item.var) }, data); if (exists === null) { - throw Error(`"${item.var}" in rule "${id}" does not exist as a JSON schema property.`); + throw Error(errorMessage(item)); } } else { checkRuleIntegrity(item, id, data); diff --git a/src/tests/jsonLogic.test.js b/src/tests/jsonLogic.test.js index 3389b4284..3ef8bf929 100644 --- a/src/tests/jsonLogic.test.js +++ b/src/tests/jsonLogic.test.js @@ -22,6 +22,7 @@ import { schemaWithGreaterThanChecksForThreeFields, schemaWithIfStatementWithComputedValuesAndValidationChecks, schemaWithInlineRuleForComputedAttributeWithCopy, + schemaWithInlinedRuleOnComputedAttributeThatReferencesUnknownVar, schemaWithMissingRule, schemaWithMultipleComputedValueChecks, schemaWithNativeAndJSONLogicChecks, @@ -178,9 +179,17 @@ describe('cross-value validations', () => { ); }); - it.todo( - 'On an inline rule for a computedAttribute, error if theres a value referenced that does not exist' - ); + it('On an inline rule for a computedAttribute, error if theres a value referenced that does not exist', () => { + createHeadlessForm(schemaWithInlinedRuleOnComputedAttributeThatReferencesUnknownVar, { + strictInputType: false, + }); + expect(console.error).toHaveBeenCalledWith( + 'JSON Schema invalid!', + Error( + '"IdontExist" in inline rule in property "field_a.x-jsf-computedAttributes.title" does not exist as a JSON schema property.' + ) + ); + }); }); describe('Relative: <, >, =', () => { @@ -515,7 +524,7 @@ describe('cross-value validations', () => { } ); const [, fieldB] = fields; - expect(handleValidation({ field_a: 0 }).formErrors).toEqual(undefined); + expect(handleValidation({ field_a: 0, field_b: null }).formErrors).toEqual(undefined); expect(fieldB.label).toEqual('I need this to work using the 10.'); expect(handleValidation({ field_a: 10 }).formErrors).toEqual(undefined); expect(fieldB.label).toEqual('I need this to work using the 20.'); From f5a9e83f8600b060087412d27e46065d012bfbca Mon Sep 17 00:00:00 2001 From: brennj Date: Wed, 9 Aug 2023 14:53:00 +0200 Subject: [PATCH 58/69] chore: renames based on RFC feedback --- src/helpers.js | 4 +-- src/jsonLogic.js | 6 ++-- src/tests/jsonLogic.test.js | 8 ++--- src/tests/jsonLogicFixtures.js | 66 +++++++++++++++++----------------- 4 files changed, 42 insertions(+), 42 deletions(-) diff --git a/src/helpers.js b/src/helpers.js index 55ce8cbc4..e61e993e1 100644 --- a/src/helpers.js +++ b/src/helpers.js @@ -473,8 +473,8 @@ export function extractParametersFromNode(schemaNode) { const presentation = pickXKey(schemaNode, 'presentation') ?? {}; const errorMessage = pickXKey(schemaNode, 'errorMessage') ?? {}; - const requiredValidations = schemaNode['x-jsf-requiredValidations']; - const computedAttributes = schemaNode['x-jsf-computedAttributes']; + const requiredValidations = schemaNode['x-jsf-logic-validations']; + const computedAttributes = schemaNode['x-jsf-logic-computedAttrs']; const node = omit(schemaNode, ['x-jsf-presentation', 'presentation']); diff --git a/src/jsonLogic.js b/src/jsonLogic.js index 093a0f9e0..cec0fb8be 100644 --- a/src/jsonLogic.js +++ b/src/jsonLogic.js @@ -303,9 +303,9 @@ function buildSampleEmptyObject(schema = {}) { function validateInlineRules(jsonSchema, sampleEmptyObject) { const properties = (jsonSchema?.properties || jsonSchema?.items?.properties) ?? {}; Object.entries(properties) - .filter(([, property]) => property['x-jsf-computedAttributes'] !== undefined) + .filter(([, property]) => property['x-jsf-logic-computedAttrs'] !== undefined) .forEach(([fieldName, property]) => { - Object.entries(property['x-jsf-computedAttributes']) + Object.entries(property['x-jsf-logic-computedAttrs']) .filter(([, value]) => typeof value === 'object') .forEach(([key, { rule }]) => { checkRuleIntegrity( @@ -313,7 +313,7 @@ function validateInlineRules(jsonSchema, sampleEmptyObject) { fieldName, sampleEmptyObject, (item) => - `"${item.var}" in inline rule in property "${fieldName}.x-jsf-computedAttributes.${key}" does not exist as a JSON schema property.` + `"${item.var}" in inline rule in property "${fieldName}.x-jsf-logic-computedAttrs.${key}" does not exist as a JSON schema property.` ); }); }); diff --git a/src/tests/jsonLogic.test.js b/src/tests/jsonLogic.test.js index 3ef8bf929..712372364 100644 --- a/src/tests/jsonLogic.test.js +++ b/src/tests/jsonLogic.test.js @@ -129,7 +129,7 @@ describe('cross-value validations', () => { ); }); - it('On x-jsf-computedAttributes, error if theres a value that does not exist.', () => { + it('On x-jsf-logic-computedAttrs, error if theres a value that does not exist.', () => { createHeadlessForm(schemaWithComputedAttributeThatDoesntExist, { strictInputType: false, }); @@ -139,7 +139,7 @@ describe('cross-value validations', () => { ); }); - it('On x-jsf-computedAttributes, error if theres a value that does not exist on a title.', () => { + it('On x-jsf-logic-computedAttrs, error if theres a value that does not exist on a title.', () => { createHeadlessForm(schemaWithComputedAttributeThatDoesntExistTitle, { strictInputType: false, }); @@ -149,7 +149,7 @@ describe('cross-value validations', () => { ); }); - it('On x-jsf-computedAttributes, error if theres a value that does not exist on a description.', () => { + it('On x-jsf-logic-computedAttrs, error if theres a value that does not exist on a description.', () => { createHeadlessForm(schemaWithComputedAttributeThatDoesntExistDescription, { strictInputType: false, }); @@ -186,7 +186,7 @@ describe('cross-value validations', () => { expect(console.error).toHaveBeenCalledWith( 'JSON Schema invalid!', Error( - '"IdontExist" in inline rule in property "field_a.x-jsf-computedAttributes.title" does not exist as a JSON schema property.' + '"IdontExist" in inline rule in property "field_a.x-jsf-logic-computedAttrs.title" does not exist as a JSON schema property.' ) ); }); diff --git a/src/tests/jsonLogicFixtures.js b/src/tests/jsonLogicFixtures.js index 39d2916c9..0f1e8d391 100644 --- a/src/tests/jsonLogicFixtures.js +++ b/src/tests/jsonLogicFixtures.js @@ -3,7 +3,7 @@ export function createSchemaWithRulesOnFieldA(rules) { properties: { field_a: { type: 'number', - 'x-jsf-requiredValidations': Object.keys(rules), + 'x-jsf-logic-validations': Object.keys(rules), }, field_b: { type: 'number', @@ -19,7 +19,7 @@ export function createSchemaWithThreePropertiesWithRuleOnFieldA(rules) { properties: { field_a: { type: 'number', - 'x-jsf-requiredValidations': Object.keys(rules), + 'x-jsf-logic-validations': Object.keys(rules), }, field_b: { type: 'number', @@ -37,7 +37,7 @@ export const schemaWithNonRequiredField = { properties: { field_a: { type: 'number', - 'x-jsf-requiredValidations': ['a_greater_than_ten'], + 'x-jsf-logic-validations': ['a_greater_than_ten'], }, }, 'x-jsf-logic': { @@ -58,7 +58,7 @@ export const schemaWithNativeAndJSONLogicChecks = { field_a: { type: 'number', minimum: 5, - 'x-jsf-requiredValidations': ['a_greater_than_ten'], + 'x-jsf-logic-validations': ['a_greater_than_ten'], }, }, 'x-jsf-logic': { @@ -78,7 +78,7 @@ export const schemaWithMissingRule = { properties: { field_a: { type: 'number', - 'x-jsf-requiredValidations': ['a_greater_than_ten'], + 'x-jsf-logic-validations': ['a_greater_than_ten'], }, }, 'x-jsf-logic': { @@ -157,7 +157,7 @@ export const schemaWithValidationThatDoesNotExistOnProperty = { properties: { field_a: { type: 'number', - 'x-jsf-requiredValidations': ['iDontExist'], + 'x-jsf-logic-validations': ['iDontExist'], }, }, }; @@ -166,7 +166,7 @@ export const schemaWithComputedAttributeThatDoesntExist = { properties: { field_a: { type: 'number', - 'x-jsf-computedAttributes': { + 'x-jsf-logic-computedAttrs': { value: 'iDontExist', }, }, @@ -177,7 +177,7 @@ export const schemaWithInlinedRuleOnComputedAttributeThatReferencesUnknownVar = properties: { field_a: { type: 'number', - 'x-jsf-computedAttributes': { + 'x-jsf-logic-computedAttrs': { title: { rule: { '+': [{ var: 'IdontExist' }], @@ -192,7 +192,7 @@ export const schemaWithComputedAttributeThatDoesntExistTitle = { properties: { field_a: { type: 'number', - 'x-jsf-computedAttributes': { + 'x-jsf-logic-computedAttrs': { title: `this doesn't exist {{iDontExist}}`, }, }, @@ -203,7 +203,7 @@ export const schemaWithComputedAttributeThatDoesntExistDescription = { properties: { field_a: { type: 'number', - 'x-jsf-computedAttributes': { + 'x-jsf-logic-computedAttrs': { description: `this doesn't exist {{iDontExist}}`, }, }, @@ -396,7 +396,7 @@ export const schemaWithChecksAndThenValidationsOnThen = { properties: { field_c: { description: 'I am a description!', - 'x-jsf-requiredValidations': ['c_must_be_large'], + 'x-jsf-logic-validations': ['c_must_be_large'], }, }, }, @@ -507,7 +507,7 @@ export const schemaWithMultipleComputedValueChecks = { required: ['field_c'], properties: { field_c: { - 'x-jsf-requiredValidations': ['double_b'], + 'x-jsf-logic-validations': ['double_b'], title: 'Adding a title.', }, }, @@ -581,7 +581,7 @@ export const multiRuleSchema = { properties: { field_a: { type: 'number', - 'x-jsf-requiredValidations': ['a_bigger_than_b', 'is_even_number'], + 'x-jsf-logic-validations': ['a_bigger_than_b', 'is_even_number'], }, field_b: { type: 'number', @@ -610,11 +610,11 @@ export const schemaWithTwoRules = { properties: { field_a: { type: 'number', - 'x-jsf-requiredValidations': ['a_bigger_than_b'], + 'x-jsf-logic-validations': ['a_bigger_than_b'], }, field_b: { type: 'number', - 'x-jsf-requiredValidations': ['is_even_number'], + 'x-jsf-logic-validations': ['is_even_number'], }, }, required: ['field_a', 'field_b'], @@ -643,7 +643,7 @@ export const schemaWithComputedAttributes = { }, field_b: { type: 'number', - 'x-jsf-computedAttributes': { + 'x-jsf-logic-computedAttrs': { title: 'This is {{a_times_two}}!', value: 'a_times_two', description: 'This field is 2 times bigger than field_a with value of {{a_times_two}}.', @@ -672,7 +672,7 @@ export const nestedFieldsetWithValidationSchema = { properties: { child: { type: 'number', - 'x-jsf-requiredValidations': ['child_greater_than_10'], + 'x-jsf-logic-validations': ['child_greater_than_10'], }, }, required: ['child'], @@ -701,11 +701,11 @@ export const validatingTwoNestedFieldsSchema = { properties: { child: { type: 'number', - 'x-jsf-requiredValidations': ['child_greater_than_10'], + 'x-jsf-logic-validations': ['child_greater_than_10'], }, other_child: { type: 'number', - 'x-jsf-requiredValidations': ['greater_than_child'], + 'x-jsf-logic-validations': ['greater_than_child'], }, }, required: ['child', 'other_child'], @@ -740,7 +740,7 @@ export const twoLevelsOfJSONLogicSchema = { properties: { child: { type: 'number', - 'x-jsf-requiredValidations': ['child_greater_than_10'], + 'x-jsf-logic-validations': ['child_greater_than_10'], }, }, required: ['child'], @@ -757,7 +757,7 @@ export const twoLevelsOfJSONLogicSchema = { }, field_b: { type: 'number', - 'x-jsf-requiredValidations': ['validation_parent', 'peek_to_nested'], + 'x-jsf-logic-validations': ['validation_parent', 'peek_to_nested'], }, }, 'x-jsf-logic': { @@ -793,7 +793,7 @@ export const fieldsetWithComputedAttributes = { other_child: { type: 'number', readOnly: true, - 'x-jsf-computedAttributes': { + 'x-jsf-logic-computedAttrs': { value: 'child_times_10', description: 'this is {{child_times_10}}', }, @@ -877,7 +877,7 @@ export const fieldsetWithAConditionalToApplyExtraValidations = { required: ['third_child'], properties: { third_child: { - 'x-jsf-requiredValidations': ['third_child_is_greater_than_other_child'], + 'x-jsf-logic-validations': ['third_child_is_greater_than_other_child'], }, }, }, @@ -904,11 +904,11 @@ export const schemaWithPropertyThatDoesNotExistInThatLevelButDoesInFieldset = { properties: { child: { type: 'number', - 'x-jsf-requiredValidations': ['child_greater_than_10'], + 'x-jsf-logic-validations': ['child_greater_than_10'], }, other_child: { type: 'number', - 'x-jsf-requiredValidations': ['greater_than_child'], + 'x-jsf-logic-validations': ['greater_than_child'], }, }, required: ['child', 'other_child'], @@ -941,7 +941,7 @@ export const simpleArrayValidationSchema = { properties: { array_item: { type: 'number', - 'x-jsf-requiredValidations': ['divisible_by_two'], + 'x-jsf-logic-validations': ['divisible_by_two'], }, }, required: ['array_item'], @@ -964,7 +964,7 @@ export const validatingASingleItemInTheArray = { properties: { field_array: { type: 'array', - 'x-jsf-requiredValidations': ['second_item_is_divisible_by_four'], + 'x-jsf-logic-validations': ['second_item_is_divisible_by_four'], items: { properties: { item: { @@ -1021,7 +1021,7 @@ export const conditionalAppliedInAnItem = { if: { validations: { divisible_by_three: { cosnt: true } } }, then: { required: ['other_item'], - other_item: { 'x-jsf-requiredValidations': ['other_item_divisible_by_three'] }, + other_item: { 'x-jsf-logic-validations': ['other_item_divisible_by_three'] }, }, else: { properties: { other_item: false } }, }, @@ -1047,7 +1047,7 @@ export const aConditionallyAppliedComputedAttributeMinimum = { then: { properties: { field_b: { - 'x-jsf-computedAttributes': { + 'x-jsf-logic-computedAttrs': { minimum: 'a_divided_by_two', 'x-jsf-errorMessage': { minimum: 'use {{a_divided_by_two}} or more', @@ -1085,7 +1085,7 @@ export const aConditionallyAppliedComputedAttributeValue = { properties: { field_b: { readOnly: true, - 'x-jsf-computedAttributes': { + 'x-jsf-logic-computedAttrs': { const: 'a_divided_by_two', value: 'a_divided_by_two', }, @@ -1119,7 +1119,7 @@ export const schemaWithInlineRuleForComputedAttributeWithCopy = { }, field_b: { type: 'number', - 'x-jsf-computedAttributes': { + 'x-jsf-logic-computedAttrs': { title: { value: 'I need this to work using the {{rule}}.', rule: { @@ -1138,7 +1138,7 @@ export const schemaWithInlineRuleForComputedAttributeWithoutCopy = { }, field_b: { type: 'number', - 'x-jsf-computedAttributes': { + 'x-jsf-logic-computedAttrs': { title: { rule: { '+': [{ var: 'field_a' }, 10], @@ -1156,7 +1156,7 @@ export const schemaWithInlineRuleForComputedAttributeWithOnlyTheRule = { }, field_b: { type: 'number', - 'x-jsf-computedAttributes': { + 'x-jsf-logic-computedAttrs': { minumum: { rule: { '+': [{ var: 'field_a' }, 10], From 725b85c589bb82e7fb2ba1983a020f681e4b0278 Mon Sep 17 00:00:00 2001 From: brennj Date: Wed, 9 Aug 2023 15:31:04 +0200 Subject: [PATCH 59/69] chore: take const into account for allowed values --- src/helpers.js | 1 + src/tests/const.test.js | 58 +++++++++++++++++++++++++++++++++++++ src/tests/jsonLogic.test.js | 5 ++-- src/yupSchema.js | 17 +++++++++++ 4 files changed, 79 insertions(+), 2 deletions(-) create mode 100644 src/tests/const.test.js diff --git a/src/helpers.js b/src/helpers.js index e61e993e1..fac0e84f0 100644 --- a/src/helpers.js +++ b/src/helpers.js @@ -485,6 +485,7 @@ export function extractParametersFromNode(schemaNode) { return omitBy( { + const: node.const, label: node.title, readOnly: node.readOnly, ...(node.deprecated && { diff --git a/src/tests/const.test.js b/src/tests/const.test.js new file mode 100644 index 000000000..4438ea070 --- /dev/null +++ b/src/tests/const.test.js @@ -0,0 +1,58 @@ +import { createHeadlessForm } from '../createHeadlessForm'; + +it('Should work for number', () => { + const { handleValidation } = createHeadlessForm( + { + properties: { + ten_only: { type: 'number', const: 10 }, + }, + }, + { strictInputType: false } + ); + expect(handleValidation({}).formErrors).toEqual(undefined); + expect(handleValidation({ ten_only: 1 }).formErrors).toEqual({ + ten_only: 'The only accepted value is 10.', + }); + expect(handleValidation({ ten_only: 10 }).formErrors).toEqual(undefined); +}); + +it('Should work for text', () => { + const { handleValidation } = createHeadlessForm( + { + properties: { + hello_only: { type: 'string', const: 'hello' }, + }, + }, + { strictInputType: false } + ); + expect(handleValidation({}).formErrors).toEqual(undefined); + expect(handleValidation({ hello_only: 'what' }).formErrors).toEqual({ + hello_only: 'The only accepted value is hello.', + }); + expect(handleValidation({ hello_only: 'hello' }).formErrors).toEqual(undefined); +}); + +it('Should work for a conditionally applied const', () => { + const { handleValidation } = createHeadlessForm( + { + properties: { + answer: { type: 'string', oneOf: [{ const: 'yes' }, { const: 'no' }] }, + amount: { description: 'If you select yes, this needs to be exactly 10.', type: 'number' }, + }, + allOf: [ + { + if: { properties: { answer: { const: 'yes' } }, required: ['answer'] }, + then: { properties: { amount: { const: 10 } }, required: ['amount'] }, + }, + ], + }, + { strictInputType: false } + ); + expect(handleValidation({}).formErrors).toEqual(undefined); + expect(handleValidation({ answer: 'no' }).formErrors).toEqual(undefined); + expect(handleValidation({ answer: 'yes' }).formErrors).toEqual({ amount: 'Required field' }); + expect(handleValidation({ answer: 'yes', amount: 1 }).formErrors).toEqual({ + amount: 'The only accepted value is 10.', + }); + expect(handleValidation({ answer: 'yes', amount: 10 }).formErrors).toEqual(undefined); +}); diff --git a/src/tests/jsonLogic.test.js b/src/tests/jsonLogic.test.js index 712372364..ba1a4cea1 100644 --- a/src/tests/jsonLogic.test.js +++ b/src/tests/jsonLogic.test.js @@ -507,8 +507,9 @@ describe('cross-value validations', () => { } ); - //TODO: this should fail because we have a const: 10, it should have an error from yup saying only 10 is accepted. - expect(handleValidation({ field_a: 20, field_b: 1 }).formErrors).toEqual(); + expect(handleValidation({ field_a: 20, field_b: 1 }).formErrors).toEqual({ + field_b: 'The only accepted value is 10.', + }); const [, fieldB] = fields; expect(fieldB.value).toEqual(10); diff --git a/src/yupSchema.js b/src/yupSchema.js index e029ddd9c..33bb45025 100644 --- a/src/yupSchema.js +++ b/src/yupSchema.js @@ -305,6 +305,19 @@ export function buildYupSchema(field, config, validations) { ); } + function withConst(yupSchema) { + return yupSchema.test( + 'isConst', + errorMessage.const ?? + errorMessageFromConfig.const ?? + `The only accepted value is ${propertyFields.const}.`, + (value) => + (propertyFields.required === false && value === undefined) || + value === null || + value === propertyFields.const + ); + } + function withBaseSchema() { const customErrorMsg = errorMessage.type || errorMessageFromConfig.type; if (customErrorMsg) { @@ -395,6 +408,10 @@ export function buildYupSchema(field, config, validations) { validators.push(withFileFormat); } + if (propertyFields.const) { + validators.push(withConst); + } + if (propertyFields.requiredValidations) { propertyFields.requiredValidations.forEach((id) => validators.push(yupSchemaWithCustomJSONLogic({ field, id, validations, config })) From 50d7ff3c61b74b51e3e36c95f8dfa662c88c99ed Mon Sep 17 00:00:00 2001 From: brennj Date: Thu, 10 Aug 2023 10:38:09 +0200 Subject: [PATCH 60/69] chore: use const + default for forced values --- src/helpers.js | 12 +++++++++++- src/tests/jsonLogic.test.js | 1 + src/tests/jsonLogicFixtures.js | 10 ++++++---- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/helpers.js b/src/helpers.js index fac0e84f0..0564f7b3f 100644 --- a/src/helpers.js +++ b/src/helpers.js @@ -476,6 +476,14 @@ export function extractParametersFromNode(schemaNode) { const requiredValidations = schemaNode['x-jsf-logic-validations']; const computedAttributes = schemaNode['x-jsf-logic-computedAttrs']; + // This is when a forced value is computed. + const decoratedComputedAttributes = { + ...(computedAttributes ?? {}), + ...(computedAttributes?.const && computedAttributes?.default + ? { value: computedAttributes.const } + : {}), + }; + const node = omit(schemaNode, ['x-jsf-presentation', 'presentation']); const description = presentation?.description || node.description; @@ -486,6 +494,8 @@ export function extractParametersFromNode(schemaNode) { return omitBy( { const: node.const, + // This is a "forced value" when both const and default are present. + ...(node.const && node.default ? { value: node.const } : {}), label: node.title, readOnly: node.readOnly, ...(node.deprecated && { @@ -522,7 +532,7 @@ export function extractParametersFromNode(schemaNode) { // Handle [name].presentation ...presentation, requiredValidations, - computedAttributes, + computedAttributes: decoratedComputedAttributes, description: containsHTML(description) ? wrapWithSpan(description, { class: 'jsf-description', diff --git a/src/tests/jsonLogic.test.js b/src/tests/jsonLogic.test.js index ba1a4cea1..e311a4313 100644 --- a/src/tests/jsonLogic.test.js +++ b/src/tests/jsonLogic.test.js @@ -481,6 +481,7 @@ describe('cross-value validations', () => { expect(fieldB.description).toEqual( 'This field is 2 times bigger than field_a with value of 4.' ); + expect(fieldB.default).toEqual(4); expect(fieldB.value).toEqual(4); expect(fieldB.label).toEqual('This is 4!'); }); diff --git a/src/tests/jsonLogicFixtures.js b/src/tests/jsonLogicFixtures.js index 0f1e8d391..94e484e47 100644 --- a/src/tests/jsonLogicFixtures.js +++ b/src/tests/jsonLogicFixtures.js @@ -167,7 +167,7 @@ export const schemaWithComputedAttributeThatDoesntExist = { field_a: { type: 'number', 'x-jsf-logic-computedAttrs': { - value: 'iDontExist', + default: 'iDontExist', }, }, }, @@ -645,7 +645,8 @@ export const schemaWithComputedAttributes = { type: 'number', 'x-jsf-logic-computedAttrs': { title: 'This is {{a_times_two}}!', - value: 'a_times_two', + const: 'a_times_two', + default: 'a_times_two', description: 'This field is 2 times bigger than field_a with value of {{a_times_two}}.', }, }, @@ -794,7 +795,8 @@ export const fieldsetWithComputedAttributes = { type: 'number', readOnly: true, 'x-jsf-logic-computedAttrs': { - value: 'child_times_10', + default: 'child_times_10', + const: 'child_times_10', description: 'this is {{child_times_10}}', }, }, @@ -1087,7 +1089,7 @@ export const aConditionallyAppliedComputedAttributeValue = { readOnly: true, 'x-jsf-logic-computedAttrs': { const: 'a_divided_by_two', - value: 'a_divided_by_two', + default: 'a_divided_by_two', }, }, }, From fda543e44a1580d4933a3c4419c8aa6cd0b7fc8e Mon Sep 17 00:00:00 2001 From: brennj Date: Fri, 11 Aug 2023 12:09:51 +0200 Subject: [PATCH 61/69] chore: replace inline values --- src/jsonLogic.js | 43 ++++++++++++++++++++++++---------- src/tests/jsonLogic.test.js | 15 ++++++++++++ src/tests/jsonLogicFixtures.js | 22 +++++++++++++++++ 3 files changed, 67 insertions(+), 13 deletions(-) diff --git a/src/jsonLogic.js b/src/jsonLogic.js index cec0fb8be..9fec72d9c 100644 --- a/src/jsonLogic.js +++ b/src/jsonLogic.js @@ -154,10 +154,25 @@ function replaceHandlebarsTemplates({ .evaluateComputedValueRuleForField(key.trim(), formValues, fieldName); }); } else if (typeof toReplace === 'object') { - const { rule, value } = toReplace; - const computedInlineValue = validations.getScope(parentID).evaluateValidation(rule, formValues); - if (value) return value.replaceAll('{{rule}}', computedInlineValue); - else return computedInlineValue; + const { value, ...rules } = toReplace; + + const ruleNames = Object.keys(rules); + if (ruleNames.length > 1 && !value) + throw Error('Cannot define multiple rules without a template string with key `value`.'); + + if (!value) + return validations.getScope(parentID).evaluateValidation(rules[ruleNames[0]], formValues); + + const computedTemplateValue = Object.entries(rules).reduce((prev, [key, rule]) => { + const computedValue = validations.getScope(parentID).evaluateValidation(rule, formValues); + return prev.replaceAll(`{{${key}}}`, computedValue); + }, value); + + return computedTemplateValue.replace(/\{\{([^{}]+)\}\}/g, (match, key) => { + return validations + .getScope(parentID) + .evaluateComputedValueRuleForField(key.trim(), formValues, fieldName); + }); } } @@ -184,7 +199,7 @@ function handleComputedAttribute(validations, formValues, parentID, name) { ]; } - if (key === 'const' || key === 'value') + if (key === 'const') return [ key, validations.getScope(parentID).evaluateComputedValueRuleForField(value, formValues, name), @@ -307,14 +322,16 @@ function validateInlineRules(jsonSchema, sampleEmptyObject) { .forEach(([fieldName, property]) => { Object.entries(property['x-jsf-logic-computedAttrs']) .filter(([, value]) => typeof value === 'object') - .forEach(([key, { rule }]) => { - checkRuleIntegrity( - rule, - fieldName, - sampleEmptyObject, - (item) => - `"${item.var}" in inline rule in property "${fieldName}.x-jsf-logic-computedAttrs.${key}" does not exist as a JSON schema property.` - ); + .forEach(([key, item]) => { + Object.values(item).forEach((rule) => { + checkRuleIntegrity( + rule, + fieldName, + sampleEmptyObject, + (item) => + `"${item.var}" in inline rule in property "${fieldName}.x-jsf-logic-computedAttrs.${key}" does not exist as a JSON schema property.` + ); + }); }); }); } diff --git a/src/tests/jsonLogic.test.js b/src/tests/jsonLogic.test.js index e311a4313..cef96eb41 100644 --- a/src/tests/jsonLogic.test.js +++ b/src/tests/jsonLogic.test.js @@ -21,6 +21,7 @@ import { schemaWithDeepVarThatDoesNotExistOnFieldset, schemaWithGreaterThanChecksForThreeFields, schemaWithIfStatementWithComputedValuesAndValidationChecks, + schemaWithInlineMultipleRulesForComputedAttributes, schemaWithInlineRuleForComputedAttributeWithCopy, schemaWithInlinedRuleOnComputedAttributeThatReferencesUnknownVar, schemaWithMissingRule, @@ -532,9 +533,23 @@ describe('cross-value validations', () => { expect(fieldB.label).toEqual('I need this to work using the 20.'); }); + it('Use multiple inline rules with different identifiers', () => { + const { fields, handleValidation } = createHeadlessForm( + schemaWithInlineMultipleRulesForComputedAttributes, + { + strictInputType: false, + } + ); + const [, fieldB] = fields; + expect(handleValidation({ field_a: 10, field_b: null }).formErrors).toEqual(undefined); + expect(fieldB.description).toEqual('Must be between 5 and 20.'); + }); + it.todo('Use a self contained rule in a schema for a title but it just uses the value'); it.todo('Use a self contained rule for a minimum value'); it.todo('Use a self contained rule for a conditionally applied schema'); + it.todo('Throw if you have multiple inline rules with no template string.'); + it.todo('Mix use of multiple inline rules and an external rule'); }); describe('Nested fieldsets', () => { diff --git a/src/tests/jsonLogicFixtures.js b/src/tests/jsonLogicFixtures.js index 94e484e47..32445a4eb 100644 --- a/src/tests/jsonLogicFixtures.js +++ b/src/tests/jsonLogicFixtures.js @@ -1178,3 +1178,25 @@ export const schemaWithInlineRuleForComputedAttributeWithOnlyTheRule = { }; export const schemaWithInlineRuleForComputedAttributeInConditionallyAppliedSchema = {}; + +export const schemaWithInlineMultipleRulesForComputedAttributes = { + properties: { + field_a: { + type: 'number', + }, + field_b: { + type: 'number', + 'x-jsf-logic-computedAttrs': { + description: { + value: 'Must be between {{half_a}} and {{double_a}}.', + half_a: { + '/': [{ var: 'field_a' }, 2], + }, + double_a: { + '*': [{ var: 'field_a' }, 2], + }, + }, + }, + }, + }, +}; From 8405442b2816876878ede23b3655eb4b79e04023 Mon Sep 17 00:00:00 2001 From: brennj Date: Fri, 11 Aug 2023 12:52:15 +0200 Subject: [PATCH 62/69] chore: fill out the todos --- src/tests/jsonLogic.test.js | 16 ++++++++++- src/tests/jsonLogicFixtures.js | 49 ++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/src/tests/jsonLogic.test.js b/src/tests/jsonLogic.test.js index cef96eb41..666eb7100 100644 --- a/src/tests/jsonLogic.test.js +++ b/src/tests/jsonLogic.test.js @@ -11,6 +11,7 @@ import { ifConditionWithMissingValidation, multiRuleSchema, nestedFieldsetWithValidationSchema, + schemaWhereValidationAndComputedValueIsAppliedOnNormalThenStatement, schemaWithChecksAndThenValidationsOnThen, schemaWithComputedAttributeThatDoesntExist, schemaWithComputedAttributeThatDoesntExistDescription, @@ -438,7 +439,20 @@ describe('cross-value validations', () => { ); }); - it.todo('Apply validations and computed values on normal if statement.'); + it('Apply validations and computed values on normal if statement.', () => { + const { fields, handleValidation } = createHeadlessForm( + schemaWhereValidationAndComputedValueIsAppliedOnNormalThenStatement, + { strictInputType: false } + ); + expect(handleValidation({ field_a: 0, field_b: 0 }).formErrors).toEqual(undefined); + expect(handleValidation({ field_a: 20, field_b: 0 }).formErrors).toEqual({ + field_b: 'Must be greater than Field A + 10', + }); + const [, fieldB] = fields; + expect(fieldB.label).toEqual('Must be greater than 30.'); + expect(handleValidation({ field_a: 20, field_b: 31 }).formErrors).toEqual(undefined); + }); + it.todo( 'When we have a required validation on a top level property and another validation is added, both should be accounted for.' ); diff --git a/src/tests/jsonLogicFixtures.js b/src/tests/jsonLogicFixtures.js index 32445a4eb..d25157f14 100644 --- a/src/tests/jsonLogicFixtures.js +++ b/src/tests/jsonLogicFixtures.js @@ -577,6 +577,55 @@ export const schemaWithIfStatementWithComputedValuesAndValidationChecks = { }, }; +export const schemaWhereValidationAndComputedValueIsAppliedOnNormalThenStatement = { + properties: { + field_a: { + type: 'number', + }, + field_b: { + type: 'number', + }, + }, + 'x-jsf-logic': { + computedValues: { + a_plus_ten: { + rule: { + '+': [{ var: 'field_a' }, 10], + }, + }, + }, + validations: { + greater_than_a_plus_ten: { + errorMessage: 'Must be greater than Field A + 10', + rule: { + '>': [{ var: 'field_b' }, { '+': [{ var: 'field_a' }, 10] }], + }, + }, + }, + }, + allOf: [ + { + if: { + properties: { + field_a: { + const: 20, + }, + }, + }, + then: { + properties: { + field_b: { + 'x-jsf-logic-computedAttrs': { + title: 'Must be greater than {{a_plus_ten}}.', + }, + 'x-jsf-logic-validations': ['greater_than_a_plus_ten'], + }, + }, + }, + }, + ], +}; + export const multiRuleSchema = { properties: { field_a: { From 80b27ff9027d38821a8e360eeaa92ab30585fce2 Mon Sep 17 00:00:00 2001 From: brennj Date: Fri, 11 Aug 2023 15:57:44 +0200 Subject: [PATCH 63/69] chore: ensure you spread requiredValidations to build up together --- src/calculateConditionalProperties.js | 6 ++++ src/tests/jsonLogic.test.js | 20 ++++++++++-- src/tests/jsonLogicFixtures.js | 47 +++++++++++++++++++++++++++ 3 files changed, 70 insertions(+), 3 deletions(-) diff --git a/src/calculateConditionalProperties.js b/src/calculateConditionalProperties.js index 074de44ac..5d7bf3e54 100644 --- a/src/calculateConditionalProperties.js +++ b/src/calculateConditionalProperties.js @@ -104,6 +104,11 @@ export function calculateConditionalProperties(fieldParams, customProperties, va ? calculateComputedAttributes(newFieldParams, config)({ validations, formValues }) : {}; + const requiredValidations = [ + ...(fieldParams.requiredValidations ?? []), + ...(restNewFieldParams.requiredValidations ?? []), + ]; + const base = { isVisible: true, required: isRequired, @@ -117,6 +122,7 @@ export function calculateConditionalProperties(fieldParams, customProperties, va ...fieldParams, ...restNewFieldParams, ...caclulatedComputedAttributes, + requiredValidations, // If there are inner fields (case of fieldset) they need to be updated based on the condition fields: fieldSetFields, required: isRequired, diff --git a/src/tests/jsonLogic.test.js b/src/tests/jsonLogic.test.js index 666eb7100..fd1fc650a 100644 --- a/src/tests/jsonLogic.test.js +++ b/src/tests/jsonLogic.test.js @@ -32,6 +32,7 @@ import { schemaWithPropertiesCheckAndValidationsInAIf, schemaWithPropertyThatDoesNotExistInThatLevelButDoesInFieldset, schemaWithTwoRules, + schemaWithTwoValidationsWhereOneOfThemIsAppliedConditionally, schemaWithValidationThatDoesNotExistOnProperty, schemaWithVarThatDoesNotExist, simpleArrayValidationSchema, @@ -453,9 +454,22 @@ describe('cross-value validations', () => { expect(handleValidation({ field_a: 20, field_b: 31 }).formErrors).toEqual(undefined); }); - it.todo( - 'When we have a required validation on a top level property and another validation is added, both should be accounted for.' - ); + it('When we have a required validation on a top level property and another validation is added, both should be accounted for.', () => { + const { handleValidation } = createHeadlessForm( + schemaWithTwoValidationsWhereOneOfThemIsAppliedConditionally, + { strictInputType: false } + ); + expect(handleValidation({ field_a: 10, field_b: 0 }).formErrors).toEqual({ + field_b: 'Must be greater than A', + }); + expect(handleValidation({ field_a: 10, field_b: 20 }).formErrors).toEqual(undefined); + expect(handleValidation({ field_a: 20, field_b: 10 }).formErrors).toEqual({ + field_b: 'Must be greater than A', + }); + expect(handleValidation({ field_a: 20, field_b: 21 }).formErrors).toEqual({ + field_b: 'Must be greater than two times A', + }); + }); }); describe('Multiple validations', () => { diff --git a/src/tests/jsonLogicFixtures.js b/src/tests/jsonLogicFixtures.js index d25157f14..bcc6cc18e 100644 --- a/src/tests/jsonLogicFixtures.js +++ b/src/tests/jsonLogicFixtures.js @@ -626,6 +626,53 @@ export const schemaWhereValidationAndComputedValueIsAppliedOnNormalThenStatement ], }; +export const schemaWithTwoValidationsWhereOneOfThemIsAppliedConditionally = { + required: ['field_a', 'field_b'], + properties: { + field_a: { + type: 'number', + }, + field_b: { + type: 'number', + 'x-jsf-logic-validations': ['greater_than_field_a'], + }, + }, + 'x-jsf-logic': { + validations: { + greater_than_field_a: { + errorMessage: 'Must be greater than A', + rule: { + '>': [{ var: 'field_b' }, { var: 'field_a' }], + }, + }, + greater_than_two_times_a: { + errorMessage: 'Must be greater than two times A', + rule: { + '>': [{ var: 'field_b' }, { '*': [{ var: 'field_a' }, 2] }], + }, + }, + }, + }, + allOf: [ + { + if: { + properties: { + field_a: { + const: 20, + }, + }, + }, + then: { + properties: { + field_b: { + 'x-jsf-logic-validations': ['greater_than_two_times_a'], + }, + }, + }, + }, + ], +}; + export const multiRuleSchema = { properties: { field_a: { From bba507bfdbc38283591350a08b18a8f96b180c87 Mon Sep 17 00:00:00 2001 From: brennj Date: Fri, 11 Aug 2023 15:59:14 +0200 Subject: [PATCH 64/69] chore: add missing little part to test --- src/tests/jsonLogic.test.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/tests/jsonLogic.test.js b/src/tests/jsonLogic.test.js index fd1fc650a..810a27e87 100644 --- a/src/tests/jsonLogic.test.js +++ b/src/tests/jsonLogic.test.js @@ -469,6 +469,7 @@ describe('cross-value validations', () => { expect(handleValidation({ field_a: 20, field_b: 21 }).formErrors).toEqual({ field_b: 'Must be greater than two times A', }); + expect(handleValidation({ field_a: 20, field_b: 41 }).formErrors).toEqual(); }); }); From 0246af8adca37ea100a3c990f277182eb2ad2d9b Mon Sep 17 00:00:00 2001 From: brennj Date: Fri, 11 Aug 2023 16:04:39 +0200 Subject: [PATCH 65/69] chore: fill out a min/max test --- src/tests/jsonLogic.test.js | 12 +++++++----- src/tests/jsonLogicFixtures.js | 9 ++++++++- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/tests/jsonLogic.test.js b/src/tests/jsonLogic.test.js index 810a27e87..e9983918d 100644 --- a/src/tests/jsonLogic.test.js +++ b/src/tests/jsonLogic.test.js @@ -1,7 +1,7 @@ import { createHeadlessForm } from '../createHeadlessForm'; import { - aConditionallyAppliedComputedAttributeMinimum, + aConditionallyAppliedComputedAttributeMinimumAndMaximum, aConditionallyAppliedComputedAttributeValue, createSchemaWithRulesOnFieldA, createSchemaWithThreePropertiesWithRuleOnFieldA, @@ -516,9 +516,9 @@ describe('cross-value validations', () => { expect(fieldB.label).toEqual('This is 4!'); }); - it('computedAttribute test that minimum, errorMessages.minimum is working', () => { + it('computedAttribute test that minimum, maximum, errorMessages.minimum, errorMessage.maximum is working', () => { const { handleValidation } = createHeadlessForm( - aConditionallyAppliedComputedAttributeMinimum, + aConditionallyAppliedComputedAttributeMinimumAndMaximum, { strictInputType: false, } @@ -526,10 +526,12 @@ describe('cross-value validations', () => { expect(handleValidation({ field_a: 20, field_b: 1 }).formErrors).toEqual({ field_b: 'use 10 or more', }); + expect(handleValidation({ field_a: 20, field_b: 60 }).formErrors).toEqual({ + field_b: 'use less than 40', + }); + expect(handleValidation({ field_a: 20, field_b: 30 }).formErrors).toEqual(undefined); }); - it.todo('computedAttribute test that maximum, errorMessages.maximum is working'); - it('Apply a conditional computed Attrbute value', () => { const { fields, handleValidation } = createHeadlessForm( aConditionallyAppliedComputedAttributeValue, diff --git a/src/tests/jsonLogicFixtures.js b/src/tests/jsonLogicFixtures.js index bcc6cc18e..e88372d09 100644 --- a/src/tests/jsonLogicFixtures.js +++ b/src/tests/jsonLogicFixtures.js @@ -1130,7 +1130,7 @@ export const conditionalAppliedInAnItem = { }, }; -export const aConditionallyAppliedComputedAttributeMinimum = { +export const aConditionallyAppliedComputedAttributeMinimumAndMaximum = { properties: { field_a: { type: 'number', @@ -1147,8 +1147,10 @@ export const aConditionallyAppliedComputedAttributeMinimum = { field_b: { 'x-jsf-logic-computedAttrs': { minimum: 'a_divided_by_two', + maximum: 'a_multiplied_by_two', 'x-jsf-errorMessage': { minimum: 'use {{a_divided_by_two}} or more', + maximum: 'use less than {{a_multiplied_by_two}}', }, }, }, @@ -1163,6 +1165,11 @@ export const aConditionallyAppliedComputedAttributeMinimum = { '/': [{ var: 'field_a' }, 2], }, }, + a_multiplied_by_two: { + rule: { + '*': [{ var: 'field_a' }, 2], + }, + }, }, }, }; From 6d7816367ac7ae28890d1d6a7922e8f08a08ccf9 Mon Sep 17 00:00:00 2001 From: brennj Date: Fri, 11 Aug 2023 17:52:35 +0200 Subject: [PATCH 66/69] chore: ensure computed attributes are counted --- src/createHeadlessForm.js | 7 +++--- src/helpers.js | 23 +++++++++--------- src/jsonLogic.js | 28 +++++++++++++++++----- src/tests/jsonLogic.test.js | 35 ++++++++++++++++++++++++--- src/tests/jsonLogicFixtures.js | 44 +++++++++++++++++++++++++++++++++- 5 files changed, 113 insertions(+), 24 deletions(-) diff --git a/src/createHeadlessForm.js b/src/createHeadlessForm.js index 6422cfd69..f979c3def 100644 --- a/src/createHeadlessForm.js +++ b/src/createHeadlessForm.js @@ -247,8 +247,9 @@ function buildField(fieldParams, config, scopedJsonSchema, validations) { customProperties ); - const caclulateComputedAttributes = - fieldParams.computedAttributes && calculateComputedAttributes(fieldParams, config); + const getComputedAttributes = + Object.keys(fieldParams.computedAttributes).length > 0 && + calculateComputedAttributes(fieldParams, config); const hasCustomValidations = !!customProperties && @@ -265,7 +266,7 @@ function buildField(fieldParams, config, scopedJsonSchema, validations) { ...(hasCustomValidations && { calculateCustomValidationProperties: calculateCustomValidationPropertiesClosure, }), - ...(caclulateComputedAttributes && { caclulateComputedAttributes }), + ...(getComputedAttributes && { getComputedAttributes }), // field customization properties ...(customProperties && { fieldCustomization: customProperties }), // base schema diff --git a/src/helpers.js b/src/helpers.js index 0564f7b3f..de9800c5d 100644 --- a/src/helpers.js +++ b/src/helpers.js @@ -217,6 +217,18 @@ function updateField(field, requiredFields, node, formValues, validations, confi } }); + if (field.getComputedAttributes) { + const computedFieldValues = field.getComputedAttributes({ + field, + isRequired: fieldIsRequired, + node, + formValues, + config, + validations, + }); + updateValues(computedFieldValues); + } + // If field has a calculateConditionalProperties closure, run it and update the field properties if (field.calculateConditionalProperties) { const newFieldValues = field.calculateConditionalProperties( @@ -237,17 +249,6 @@ function updateField(field, requiredFields, node, formValues, validations, confi ); updateValues(newFieldValues); } - - if (field.caclulateComputedAttributes) { - const computedFieldValues = field.caclulateComputedAttributes({ - field, - isRequired: fieldIsRequired, - node, - formValues, - validations, - }); - updateValues(computedFieldValues); - } } /** diff --git a/src/jsonLogic.js b/src/jsonLogic.js index 9fec72d9c..bba8216d3 100644 --- a/src/jsonLogic.js +++ b/src/jsonLogic.js @@ -5,6 +5,7 @@ import { checkIfMatchesValidationsAndComputedValues, } from './checkIfConditionMatches'; import { processNode } from './helpers'; +import { buildYupSchema } from './yupSchema'; jsonLogic.add_operation('Number.toFixed', (a, b) => { if (typeof a === 'number') return a.toFixed(b); @@ -177,13 +178,22 @@ function replaceHandlebarsTemplates({ } export function calculateComputedAttributes(fieldParams, { parentID = 'root' } = {}) { - return ({ validations, formValues }) => { + return ({ validations, isRequired, config, formValues }) => { const { name, computedAttributes } = fieldParams; - return Object.fromEntries( + const attributes = Object.fromEntries( Object.entries(computedAttributes) .map(handleComputedAttribute(validations, formValues, parentID, name)) .filter(([, value]) => value !== null) ); + + return { + ...attributes, + schema: buildYupSchema( + { ...fieldParams, ...attributes, required: isRequired }, + config, + validations + ), + }; }; } @@ -212,6 +222,13 @@ function handleComputedAttribute(validations, formValues, parentID, name) { ]; } + if (typeof value === 'string') { + return [ + key, + validations.getScope(parentID).evaluateComputedValueRuleForField(value, formValues, name), + ]; + } + if (key === 'x-jsf-presentation' && value.statement) { return [ 'statement', @@ -219,10 +236,9 @@ function handleComputedAttribute(validations, formValues, parentID, name) { ]; } - return [ - key, - validations.getScope(parentID).evaluateComputedValueRuleForField(value, formValues, name), - ]; + if (typeof value === 'object' && value.rule) { + return [key, validations.getScope(parentID).evaluateValidation(value.rule, formValues)]; + } }; } diff --git a/src/tests/jsonLogic.test.js b/src/tests/jsonLogic.test.js index e9983918d..c323b8cef 100644 --- a/src/tests/jsonLogic.test.js +++ b/src/tests/jsonLogic.test.js @@ -11,6 +11,8 @@ import { ifConditionWithMissingValidation, multiRuleSchema, nestedFieldsetWithValidationSchema, + schemaSelfContainedValueForMaximumMinimumValues, + schemaSelfContainedValueForTitleWithNoTemplate, schemaWhereValidationAndComputedValueIsAppliedOnNormalThenStatement, schemaWithChecksAndThenValidationsOnThen, schemaWithComputedAttributeThatDoesntExist, @@ -532,7 +534,7 @@ describe('cross-value validations', () => { expect(handleValidation({ field_a: 20, field_b: 30 }).formErrors).toEqual(undefined); }); - it('Apply a conditional computed Attrbute value', () => { + it('Apply a conditional computed attribute value', () => { const { fields, handleValidation } = createHeadlessForm( aConditionallyAppliedComputedAttributeValue, { @@ -576,8 +578,35 @@ describe('cross-value validations', () => { expect(fieldB.description).toEqual('Must be between 5 and 20.'); }); - it.todo('Use a self contained rule in a schema for a title but it just uses the value'); - it.todo('Use a self contained rule for a minimum value'); + it('Use a self contained rule in a schema for a title but it just uses the value', () => { + const { fields, handleValidation } = createHeadlessForm( + schemaSelfContainedValueForTitleWithNoTemplate, + { + strictInputType: false, + } + ); + const [, fieldB] = fields; + expect(handleValidation({ field_a: 10, field_b: null }).formErrors).toEqual(undefined); + expect(fieldB.label).toEqual('20'); + }); + + it('Use a self contained rule for a minimum, maximum value', () => { + const { handleValidation } = createHeadlessForm( + schemaSelfContainedValueForMaximumMinimumValues, + { + strictInputType: false, + } + ); + expect(handleValidation({ field_a: 10, field_b: null }).formErrors).toEqual(undefined); + expect(handleValidation({ field_a: 50, field_b: 20 }).formErrors).toEqual({ + field_b: 'Must be greater or equal to 40', + }); + expect(handleValidation({ field_a: 50, field_b: 70 }).formErrors).toEqual({ + field_b: 'Must be smaller or equal to 60', + }); + expect(handleValidation({ field_a: 50, field_b: 50 }).formErrors).toEqual(undefined); + }); + it.todo('Use a self contained rule for a conditionally applied schema'); it.todo('Throw if you have multiple inline rules with no template string.'); it.todo('Mix use of multiple inline rules and an external rule'); diff --git a/src/tests/jsonLogicFixtures.js b/src/tests/jsonLogicFixtures.js index e88372d09..23f01dabc 100644 --- a/src/tests/jsonLogicFixtures.js +++ b/src/tests/jsonLogicFixtures.js @@ -1262,7 +1262,7 @@ export const schemaWithInlineRuleForComputedAttributeWithOnlyTheRule = { field_b: { type: 'number', 'x-jsf-logic-computedAttrs': { - minumum: { + minimum: { rule: { '+': [{ var: 'field_a' }, 10], }, @@ -1303,3 +1303,45 @@ export const schemaWithInlineMultipleRulesForComputedAttributes = { }, }, }; + +export const schemaSelfContainedValueForTitleWithNoTemplate = { + properties: { + field_a: { + type: 'number', + }, + field_b: { + type: 'number', + 'x-jsf-logic-computedAttrs': { + title: { + value: '{{rule}}', + rule: { + '+': [{ var: 'field_a' }, 10], + }, + }, + }, + }, + }, +}; + +export const schemaSelfContainedValueForMaximumMinimumValues = { + properties: { + field_a: { + type: 'number', + }, + field_b: { + type: 'number', + 'x-jsf-logic-computedAttrs': { + maximum: { + rule: { + '+': [{ var: 'field_a' }, 10], + }, + }, + minimum: { + rule: { + '-': [{ var: 'field_a' }, 10], + }, + }, + }, + }, + }, +}; From 0274f9b3e7f5045f9f5973780ca3a395e7ad3893 Mon Sep 17 00:00:00 2001 From: brennj Date: Fri, 11 Aug 2023 18:07:58 +0200 Subject: [PATCH 67/69] chore: error unit tests --- src/jsonLogic.js | 4 ---- src/tests/jsonLogic.test.js | 18 +++++++++++++++++ src/tests/jsonLogicFixtures.js | 36 ++++++++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 4 deletions(-) diff --git a/src/jsonLogic.js b/src/jsonLogic.js index bba8216d3..687c85de9 100644 --- a/src/jsonLogic.js +++ b/src/jsonLogic.js @@ -88,10 +88,6 @@ function createValidationsScope(schema) { validationMap, computedValuesMap, evaluateValidation, - evaluateValidationRule(id, values) { - const validation = validationMap.get(id); - return evaluateValidation(validation.rule, values); - }, evaluateValidationRuleInCondition(id, values) { const validation = validationMap.get(id); if (validation === undefined) { diff --git a/src/tests/jsonLogic.test.js b/src/tests/jsonLogic.test.js index c323b8cef..b1da6c6f3 100644 --- a/src/tests/jsonLogic.test.js +++ b/src/tests/jsonLogic.test.js @@ -27,7 +27,9 @@ import { schemaWithInlineMultipleRulesForComputedAttributes, schemaWithInlineRuleForComputedAttributeWithCopy, schemaWithInlinedRuleOnComputedAttributeThatReferencesUnknownVar, + schemaWithMissingComputedValue, schemaWithMissingRule, + schemaWithMissingValueInlineRule, schemaWithMultipleComputedValueChecks, schemaWithNativeAndJSONLogicChecks, schemaWithNonRequiredField, @@ -90,6 +92,22 @@ describe('cross-value validations', () => { ); }); + it('Should throw when theres a missing computed value', () => { + createHeadlessForm(schemaWithMissingComputedValue, { strictInputType: false }); + expect(console.error).toHaveBeenCalledWith( + 'JSON Schema invalid!', + Error('Missing rule for computedValue with id of: "a_plus_ten".') + ); + }); + + it('Should throw when theres an inline computed ruleset with no value.', () => { + createHeadlessForm(schemaWithMissingValueInlineRule, { strictInputType: false }); + expect(console.error).toHaveBeenCalledWith( + 'JSON Schema invalid!', + Error('Cannot define multiple rules without a template string with key `value`.') + ); + }); + it('Should throw when a var does not exist in a rule.', () => { createHeadlessForm(schemaWithVarThatDoesNotExist, { strictInputType: false }); expect(console.error).toHaveBeenCalledWith( diff --git a/src/tests/jsonLogicFixtures.js b/src/tests/jsonLogicFixtures.js index 23f01dabc..a8bcbc241 100644 --- a/src/tests/jsonLogicFixtures.js +++ b/src/tests/jsonLogicFixtures.js @@ -91,6 +91,42 @@ export const schemaWithMissingRule = { required: [], }; +export const schemaWithMissingComputedValue = { + properties: { + field_a: { + type: 'number', + 'x-jsf-logic-computedAttrs': { + title: '{{a_plus_ten}}', + }, + }, + }, + 'x-jsf-logic': { + computedValues: { + a_plus_ten: {}, + }, + }, + required: [], +}; + +export const schemaWithMissingValueInlineRule = { + properties: { + field_a: { + type: 'number', + 'x-jsf-logic-computedAttrs': { + title: { + ruleOne: { + '+': [1, 2], + }, + ruleTwo: { + '+': [3, 4], + }, + }, + }, + }, + }, + required: [], +}; + export const schemaWithVarThatDoesNotExist = { properties: { field_a: { From 748d46587f075d4e29d06097bcfea3d72d90bd9f Mon Sep 17 00:00:00 2001 From: brennj Date: Fri, 11 Aug 2023 18:14:25 +0200 Subject: [PATCH 68/69] chore: clean up a little --- src/jsonLogic.js | 6 +----- src/tests/jsonLogic.test.js | 12 ++++++++++-- src/tests/jsonLogicFixtures.js | 28 ++++++++++++++++++++++++++++ 3 files changed, 39 insertions(+), 7 deletions(-) diff --git a/src/jsonLogic.js b/src/jsonLogic.js index 687c85de9..21c166a24 100644 --- a/src/jsonLogic.js +++ b/src/jsonLogic.js @@ -153,13 +153,9 @@ function replaceHandlebarsTemplates({ } else if (typeof toReplace === 'object') { const { value, ...rules } = toReplace; - const ruleNames = Object.keys(rules); - if (ruleNames.length > 1 && !value) + if (Object.keys(rules).length > 1 && !value) throw Error('Cannot define multiple rules without a template string with key `value`.'); - if (!value) - return validations.getScope(parentID).evaluateValidation(rules[ruleNames[0]], formValues); - const computedTemplateValue = Object.entries(rules).reduce((prev, [key, rule]) => { const computedValue = validations.getScope(parentID).evaluateValidation(rule, formValues); return prev.replaceAll(`{{${key}}}`, computedValue); diff --git a/src/tests/jsonLogic.test.js b/src/tests/jsonLogic.test.js index b1da6c6f3..04f07f774 100644 --- a/src/tests/jsonLogic.test.js +++ b/src/tests/jsonLogic.test.js @@ -27,6 +27,7 @@ import { schemaWithInlineMultipleRulesForComputedAttributes, schemaWithInlineRuleForComputedAttributeWithCopy, schemaWithInlinedRuleOnComputedAttributeThatReferencesUnknownVar, + schemaWithJSFLogicAndInlineRule, schemaWithMissingComputedValue, schemaWithMissingRule, schemaWithMissingValueInlineRule, @@ -626,8 +627,15 @@ describe('cross-value validations', () => { }); it.todo('Use a self contained rule for a conditionally applied schema'); - it.todo('Throw if you have multiple inline rules with no template string.'); - it.todo('Mix use of multiple inline rules and an external rule'); + + it('Mix use of multiple inline rules and an external rule', () => { + const { fields, handleValidation } = createHeadlessForm(schemaWithJSFLogicAndInlineRule, { + strictInputType: false, + }); + handleValidation({ field_a: 10 }); + const [, fieldB] = fields; + expect(fieldB.label).toEqual('Going to use 20 and 4'); + }); }); describe('Nested fieldsets', () => { diff --git a/src/tests/jsonLogicFixtures.js b/src/tests/jsonLogicFixtures.js index a8bcbc241..9c53ba586 100644 --- a/src/tests/jsonLogicFixtures.js +++ b/src/tests/jsonLogicFixtures.js @@ -1381,3 +1381,31 @@ export const schemaSelfContainedValueForMaximumMinimumValues = { }, }, }; + +export const schemaWithJSFLogicAndInlineRule = { + properties: { + field_a: { + type: 'number', + }, + field_b: { + type: 'number', + 'x-jsf-logic-computedAttrs': { + title: { + value: 'Going to use {{rule}} and {{not_inline}}', + rule: { + '+': [{ var: 'field_a' }, 10], + }, + }, + }, + }, + }, + 'x-jsf-logic': { + computedValues: { + not_inline: { + rule: { + '+': [1, 3], + }, + }, + }, + }, +}; From 7546f2ef0b2b7c12b0b3290ae1d506c29a6c6838 Mon Sep 17 00:00:00 2001 From: brennj Date: Fri, 11 Aug 2023 18:27:45 +0200 Subject: [PATCH 69/69] chore: code complete? --- src/calculateConditionalProperties.js | 10 +++---- src/tests/jsonLogic.test.js | 14 ++++++++- src/tests/jsonLogicFixtures.js | 41 ++++++++++++++++++++++++++- 3 files changed, 58 insertions(+), 7 deletions(-) diff --git a/src/calculateConditionalProperties.js b/src/calculateConditionalProperties.js index 5d7bf3e54..0a4825b89 100644 --- a/src/calculateConditionalProperties.js +++ b/src/calculateConditionalProperties.js @@ -100,7 +100,7 @@ export function calculateConditionalProperties(fieldParams, customProperties, va } const { computedAttributes, ...restNewFieldParams } = newFieldParams; - const caclulatedComputedAttributes = computedAttributes + const calculatedComputedAttributes = computedAttributes ? calculateComputedAttributes(newFieldParams, config)({ validations, formValues }) : {}; @@ -113,15 +113,15 @@ export function calculateConditionalProperties(fieldParams, customProperties, va isVisible: true, required: isRequired, ...(presentation?.inputType && { type: presentation.inputType }), - ...caclulatedComputedAttributes, - ...(caclulatedComputedAttributes.value - ? { value: caclulatedComputedAttributes.value } + ...calculatedComputedAttributes, + ...(calculatedComputedAttributes.value + ? { value: calculatedComputedAttributes.value } : { value: undefined }), schema: buildYupSchema( { ...fieldParams, ...restNewFieldParams, - ...caclulatedComputedAttributes, + ...calculatedComputedAttributes, requiredValidations, // If there are inner fields (case of fieldset) they need to be updated based on the condition fields: fieldSetFields, diff --git a/src/tests/jsonLogic.test.js b/src/tests/jsonLogic.test.js index 04f07f774..ef2d5f147 100644 --- a/src/tests/jsonLogic.test.js +++ b/src/tests/jsonLogic.test.js @@ -25,6 +25,7 @@ import { schemaWithGreaterThanChecksForThreeFields, schemaWithIfStatementWithComputedValuesAndValidationChecks, schemaWithInlineMultipleRulesForComputedAttributes, + schemaWithInlineRuleForComputedAttributeInConditionallyAppliedSchema, schemaWithInlineRuleForComputedAttributeWithCopy, schemaWithInlinedRuleOnComputedAttributeThatReferencesUnknownVar, schemaWithJSFLogicAndInlineRule, @@ -626,7 +627,18 @@ describe('cross-value validations', () => { expect(handleValidation({ field_a: 50, field_b: 50 }).formErrors).toEqual(undefined); }); - it.todo('Use a self contained rule for a conditionally applied schema'); + it('Use a self contained rule for a conditionally applied schema', () => { + const { fields, handleValidation } = createHeadlessForm( + schemaWithInlineRuleForComputedAttributeInConditionallyAppliedSchema, + { + strictInputType: false, + } + ); + const [, fieldB] = fields; + expect(fieldB.description).toEqual('Hello world'); + handleValidation({ field_a: 20, field_b: 0 }); + expect(fieldB.description).toEqual('Must be between 10 and 40.'); + }); it('Mix use of multiple inline rules and an external rule', () => { const { fields, handleValidation } = createHeadlessForm(schemaWithJSFLogicAndInlineRule, { diff --git a/src/tests/jsonLogicFixtures.js b/src/tests/jsonLogicFixtures.js index 9c53ba586..96e11f539 100644 --- a/src/tests/jsonLogicFixtures.js +++ b/src/tests/jsonLogicFixtures.js @@ -1316,7 +1316,46 @@ export const schemaWithInlineRuleForComputedAttributeWithOnlyTheRule = { }, }; -export const schemaWithInlineRuleForComputedAttributeInConditionallyAppliedSchema = {}; +export const schemaWithInlineRuleForComputedAttributeInConditionallyAppliedSchema = { + properties: { + field_a: { + type: 'number', + }, + field_b: { + description: 'Hello world', + type: 'number', + }, + }, + allOf: [ + { + if: { + properties: { + field_a: { + const: 20, + }, + }, + required: ['field_a'], + }, + then: { + properties: { + field_b: { + 'x-jsf-logic-computedAttrs': { + description: { + value: 'Must be between {{half_a}} and {{double_a}}.', + half_a: { + '/': [{ var: 'field_a' }, 2], + }, + double_a: { + '*': [{ var: 'field_a' }, 2], + }, + }, + }, + }, + }, + }, + }, + ], +}; export const schemaWithInlineMultipleRulesForComputedAttributes = { properties: {