diff --git a/.github/workflows/budibase_ci.yml b/.github/workflows/budibase_ci.yml index 86fd4f6799..288a0462e7 100644 --- a/.github/workflows/budibase_ci.yml +++ b/.github/workflows/budibase_ci.yml @@ -214,6 +214,7 @@ jobs: echo "pro_commit=$pro_commit" echo "pro_commit=$pro_commit" >> "$GITHUB_OUTPUT" echo "base_commit=$base_commit" + echo "base_commit=$base_commit" >> "$GITHUB_OUTPUT" base_commit_excluding_merges=$(git log --no-merges -n 1 --format=format:%H $base_commit) echo "base_commit_excluding_merges=$base_commit_excluding_merges" @@ -230,7 +231,7 @@ jobs: base_commit_excluding_merges='${{ steps.get_pro_commits.outputs.base_commit_excluding_merges }}' pro_commit='${{ steps.get_pro_commits.outputs.pro_commit }}' - any_commit=$(git log --no-merges $base_commit...$pro_commit) + any_commit=$(git log --no-merges $base_commit_excluding_merges...$pro_commit) if [ -n "$any_commit" ]; then echo $any_commit diff --git a/examples/nextjs-api-sales/package.json b/examples/nextjs-api-sales/package.json index 481197b26c..7ecf264add 100644 --- a/examples/nextjs-api-sales/package.json +++ b/examples/nextjs-api-sales/package.json @@ -10,7 +10,7 @@ }, "dependencies": { "bulma": "^0.9.3", - "next": "12.1.0", + "next": "14.1.1", "node-fetch": "^3.2.10", "sass": "^1.52.3", "react": "17.0.2", diff --git a/examples/nextjs-api-sales/yarn.lock b/examples/nextjs-api-sales/yarn.lock index 93e26a954d..2c36066211 100644 --- a/examples/nextjs-api-sales/yarn.lock +++ b/examples/nextjs-api-sales/yarn.lock @@ -46,10 +46,10 @@ resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== -"@next/env@12.1.0": - version "12.1.0" - resolved "https://registry.yarnpkg.com/@next/env/-/env-12.1.0.tgz#73713399399b34aa5a01771fb73272b55b22c314" - integrity sha512-nrIgY6t17FQ9xxwH3jj0a6EOiQ/WDHUos35Hghtr+SWN/ntHIQ7UpuvSi0vaLzZVHQWaDupKI+liO5vANcDeTQ== +"@next/env@14.1.1": + version "14.1.1" + resolved "https://registry.yarnpkg.com/@next/env/-/env-14.1.1.tgz#80150a8440eb0022a73ba353c6088d419b908bac" + integrity sha512-7CnQyD5G8shHxQIIg3c7/pSeYFeMhsNbpU/bmvH7ZnDql7mNRgg8O2JZrhrc/soFnfBnKP4/xXNiiSIPn2w8gA== "@next/eslint-plugin-next@12.1.0": version "12.1.0" @@ -58,60 +58,50 @@ dependencies: glob "7.1.7" -"@next/swc-android-arm64@12.1.0": - version "12.1.0" - resolved "https://registry.yarnpkg.com/@next/swc-android-arm64/-/swc-android-arm64-12.1.0.tgz#865ba3a9afc204ff2bdeea49dd64d58705007a39" - integrity sha512-/280MLdZe0W03stA69iL+v6I+J1ascrQ6FrXBlXGCsGzrfMaGr7fskMa0T5AhQIVQD4nA/46QQWxG//DYuFBcA== +"@next/swc-darwin-arm64@14.1.1": + version "14.1.1" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.1.1.tgz#b74ba7c14af7d05fa2848bdeb8ee87716c939b64" + integrity sha512-yDjSFKQKTIjyT7cFv+DqQfW5jsD+tVxXTckSe1KIouKk75t1qZmj/mV3wzdmFb0XHVGtyRjDMulfVG8uCKemOQ== -"@next/swc-darwin-arm64@12.1.0": - version "12.1.0" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-12.1.0.tgz#08e8b411b8accd095009ed12efbc2f1d4d547135" - integrity sha512-R8vcXE2/iONJ1Unf5Ptqjk6LRW3bggH+8drNkkzH4FLEQkHtELhvcmJwkXcuipyQCsIakldAXhRbZmm3YN1vXg== +"@next/swc-darwin-x64@14.1.1": + version "14.1.1" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-14.1.1.tgz#82c3e67775e40094c66e76845d1a36cc29c9e78b" + integrity sha512-KCQmBL0CmFmN8D64FHIZVD9I4ugQsDBBEJKiblXGgwn7wBCSe8N4Dx47sdzl4JAg39IkSN5NNrr8AniXLMb3aw== -"@next/swc-darwin-x64@12.1.0": - version "12.1.0" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-12.1.0.tgz#fcd684497a76e8feaca88db3c394480ff0b007cd" - integrity sha512-ieAz0/J0PhmbZBB8+EA/JGdhRHBogF8BWaeqR7hwveb6SYEIJaDNQy0I+ZN8gF8hLj63bEDxJAs/cEhdnTq+ug== +"@next/swc-linux-arm64-gnu@14.1.1": + version "14.1.1" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.1.1.tgz#4f4134457b90adc5c3d167d07dfb713c632c0caa" + integrity sha512-YDQfbWyW0JMKhJf/T4eyFr4b3tceTorQ5w2n7I0mNVTFOvu6CGEzfwT3RSAQGTi/FFMTFcuspPec/7dFHuP7Eg== -"@next/swc-linux-arm-gnueabihf@12.1.0": - version "12.1.0" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-12.1.0.tgz#9ec6380a27938a5799aaa6035c205b3c478468a7" - integrity sha512-njUd9hpl6o6A5d08dC0cKAgXKCzm5fFtgGe6i0eko8IAdtAPbtHxtpre3VeSxdZvuGFh+hb0REySQP9T1ttkog== +"@next/swc-linux-arm64-musl@14.1.1": + version "14.1.1" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.1.1.tgz#594bedafaeba4a56db23a48ffed2cef7cd09c31a" + integrity sha512-fiuN/OG6sNGRN/bRFxRvV5LyzLB8gaL8cbDH5o3mEiVwfcMzyE5T//ilMmaTrnA8HLMS6hoz4cHOu6Qcp9vxgQ== -"@next/swc-linux-arm64-gnu@12.1.0": - version "12.1.0" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-12.1.0.tgz#7f4196dff1049cea479607c75b81033ae2dbd093" - integrity sha512-OqangJLkRxVxMhDtcb7Qn1xjzFA3s50EIxY7mljbSCLybU+sByPaWAHY4px97ieOlr2y4S0xdPKkQ3BCAwyo6Q== +"@next/swc-linux-x64-gnu@14.1.1": + version "14.1.1" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.1.1.tgz#cb4e75f1ff2b9bcadf2a50684605928ddfc58528" + integrity sha512-rv6AAdEXoezjbdfp3ouMuVqeLjE1Bin0AuE6qxE6V9g3Giz5/R3xpocHoAi7CufRR+lnkuUjRBn05SYJ83oKNQ== -"@next/swc-linux-arm64-musl@12.1.0": - version "12.1.0" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-12.1.0.tgz#b445f767569cdc2dddee785ca495e1a88c025566" - integrity sha512-hB8cLSt4GdmOpcwRe2UzI5UWn6HHO/vLkr5OTuNvCJ5xGDwpPXelVkYW/0+C3g5axbDW2Tym4S+MQCkkH9QfWA== +"@next/swc-linux-x64-musl@14.1.1": + version "14.1.1" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.1.1.tgz#15f26800df941b94d06327f674819ab64b272e25" + integrity sha512-YAZLGsaNeChSrpz/G7MxO3TIBLaMN8QWMr3X8bt6rCvKovwU7GqQlDu99WdvF33kI8ZahvcdbFsy4jAFzFX7og== -"@next/swc-linux-x64-gnu@12.1.0": - version "12.1.0" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-12.1.0.tgz#67610e9be4fbc987de7535f1bcb17e45fe12f90e" - integrity sha512-OKO4R/digvrVuweSw/uBM4nSdyzsBV5EwkUeeG4KVpkIZEe64ZwRpnFB65bC6hGwxIBnTv5NMSnJ+0K/WmG78A== +"@next/swc-win32-arm64-msvc@14.1.1": + version "14.1.1" + resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.1.1.tgz#060c134fa7fa843666e3e8574972b2b723773dd9" + integrity sha512-1L4mUYPBMvVDMZg1inUYyPvFSduot0g73hgfD9CODgbr4xiTYe0VOMTZzaRqYJYBA9mana0x4eaAaypmWo1r5A== -"@next/swc-linux-x64-musl@12.1.0": - version "12.1.0" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-12.1.0.tgz#ea19a23db08a9f2e34ac30401f774cf7d1669d31" - integrity sha512-JohhgAHZvOD3rQY7tlp7NlmvtvYHBYgY0x5ZCecUT6eCCcl9lv6iV3nfu82ErkxNk1H893fqH0FUpznZ/H3pSw== +"@next/swc-win32-ia32-msvc@14.1.1": + version "14.1.1" + resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.1.1.tgz#5c06889352b1f77e3807834a0d0afd7e2d2d1da2" + integrity sha512-jvIE9tsuj9vpbbXlR5YxrghRfMuG0Qm/nZ/1KDHc+y6FpnZ/apsgh+G6t15vefU0zp3WSpTMIdXRUsNl/7RSuw== -"@next/swc-win32-arm64-msvc@12.1.0": - version "12.1.0" - resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-12.1.0.tgz#eadf054fc412085659b98e145435bbba200b5283" - integrity sha512-T/3gIE6QEfKIJ4dmJk75v9hhNiYZhQYAoYm4iVo1TgcsuaKLFa+zMPh4056AHiG6n9tn2UQ1CFE8EoybEsqsSw== - -"@next/swc-win32-ia32-msvc@12.1.0": - version "12.1.0" - resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-12.1.0.tgz#68faeae10c89f698bf9d28759172b74c9c21bda1" - integrity sha512-iwnKgHJdqhIW19H9PRPM9j55V6RdcOo6rX+5imx832BCWzkDbyomWnlzBfr6ByUYfhohb8QuH4hSGEikpPqI0Q== - -"@next/swc-win32-x64-msvc@12.1.0": - version "12.1.0" - resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-12.1.0.tgz#d27e7e76c87a460a4da99c5bfdb1618dcd6cd064" - integrity sha512-aBvcbMwuanDH4EMrL2TthNJy+4nP59Bimn8egqv6GHMVj0a44cU6Au4PjOhLNqEh9l+IpRGBqMTzec94UdC5xg== +"@next/swc-win32-x64-msvc@14.1.1": + version "14.1.1" + resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.1.1.tgz#d38c63a8f9b7f36c1470872797d3735b4a9c5c52" + integrity sha512-S6K6EHDU5+1KrBDLko7/c1MNy/Ya73pIAmvKeFwsF4RmBFJSO7/7YeD4FnZ4iBdzE69PpQ4sOMU9ORKeNuxe8A== "@nodelib/fs.scandir@2.1.5": version "2.1.5" @@ -139,6 +129,13 @@ resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.1.0.tgz#7f698254aadf921e48dda8c0a6b304026b8a9323" integrity sha512-JLo+Y592QzIE+q7Dl2pMUtt4q8SKYI5jDrZxrozEQxnGVOyYE+GWK9eLkwTaeN9DDctlaRAQ3TBmzZ1qdLE30A== +"@swc/helpers@0.5.2": + version "0.5.2" + resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.2.tgz#85ea0c76450b61ad7d10a37050289eded783c27d" + integrity sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw== + dependencies: + tslib "^2.4.0" + "@types/json5@^0.0.29": version "0.0.29" resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" @@ -344,6 +341,13 @@ bulma@^0.9.3: resolved "https://registry.yarnpkg.com/bulma/-/bulma-0.9.3.tgz#ddccb7436ebe3e21bf47afe01d3c43a296b70243" integrity sha512-0d7GNW1PY4ud8TWxdNcP6Cc8Bu7MxcntD/RRLGWuiw/s0a9P+XlH/6QoOIrmbj6o8WWJzJYhytiu9nFjTszk1g== +busboy@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/busboy/-/busboy-1.6.0.tgz#966ea36a9502e43cdb9146962523b92f531f6893" + integrity sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA== + dependencies: + streamsearch "^1.1.0" + call-bind@^1.0.0, call-bind@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" @@ -357,10 +361,10 @@ callsites@^3.0.0: resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== -caniuse-lite@^1.0.30001283: - version "1.0.30001314" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001314.tgz#65c7f9fb7e4594fca0a333bec1d8939662377596" - integrity sha512-0zaSO+TnCHtHJIbpLroX7nsD+vYuOVjl3uzFbJO1wMVbuveJA0RK2WcQA9ZUIOiO0/ArMiMgHJLxfEZhQiC0kw== +caniuse-lite@^1.0.30001579: + version "1.0.30001640" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001640.tgz#32c467d4bf1f1a0faa63fc793c2ba81169e7652f" + integrity sha512-lA4VMpW0PSUrFnkmVuEKBUovSWKhj7puyCg8StBChgu298N1AtuF1sKWEvfDuimSEDbhlb/KqPKC3fs1HbuQUA== chalk@^4.0.0: version "4.1.2" @@ -385,6 +389,11 @@ chalk@^4.0.0: optionalDependencies: fsevents "~2.3.2" +client-only@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1" + integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA== + color-convert@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" @@ -909,6 +918,11 @@ globby@^11.0.4: merge2 "^1.4.1" slash "^3.0.0" +graceful-fs@^4.2.11: + version "4.2.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + has-bigints@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.1.tgz#64fe6acb020673e3b78db035a5af69aa9d07b113" @@ -1221,38 +1235,38 @@ ms@^2.1.1: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== -nanoid@^3.1.30: - version "3.3.1" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.1.tgz#6347a18cac88af88f58af0b3594b723d5e99bb35" - integrity sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw== +nanoid@^3.3.6: + version "3.3.7" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" + integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g== natural-compare@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= -next@12.1.0: - version "12.1.0" - resolved "https://registry.yarnpkg.com/next/-/next-12.1.0.tgz#c33d753b644be92fc58e06e5a214f143da61dd5d" - integrity sha512-s885kWvnIlxsUFHq9UGyIyLiuD0G3BUC/xrH0CEnH5lHEWkwQcHOORgbDF0hbrW9vr/7am4ETfX4A7M6DjrE7Q== +next@14.1.1: + version "14.1.1" + resolved "https://registry.yarnpkg.com/next/-/next-14.1.1.tgz#92bd603996c050422a738e90362dff758459a171" + integrity sha512-McrGJqlGSHeaz2yTRPkEucxQKe5Zq7uPwyeHNmJaZNY4wx9E9QdxmTp310agFRoMuIYgQrCrT3petg13fSVOww== dependencies: - "@next/env" "12.1.0" - caniuse-lite "^1.0.30001283" - postcss "8.4.5" - styled-jsx "5.0.0" - use-subscription "1.5.1" + "@next/env" "14.1.1" + "@swc/helpers" "0.5.2" + busboy "1.6.0" + caniuse-lite "^1.0.30001579" + graceful-fs "^4.2.11" + postcss "8.4.31" + styled-jsx "5.1.1" optionalDependencies: - "@next/swc-android-arm64" "12.1.0" - "@next/swc-darwin-arm64" "12.1.0" - "@next/swc-darwin-x64" "12.1.0" - "@next/swc-linux-arm-gnueabihf" "12.1.0" - "@next/swc-linux-arm64-gnu" "12.1.0" - "@next/swc-linux-arm64-musl" "12.1.0" - "@next/swc-linux-x64-gnu" "12.1.0" - "@next/swc-linux-x64-musl" "12.1.0" - "@next/swc-win32-arm64-msvc" "12.1.0" - "@next/swc-win32-ia32-msvc" "12.1.0" - "@next/swc-win32-x64-msvc" "12.1.0" + "@next/swc-darwin-arm64" "14.1.1" + "@next/swc-darwin-x64" "14.1.1" + "@next/swc-linux-arm64-gnu" "14.1.1" + "@next/swc-linux-arm64-musl" "14.1.1" + "@next/swc-linux-x64-gnu" "14.1.1" + "@next/swc-linux-x64-musl" "14.1.1" + "@next/swc-win32-arm64-msvc" "14.1.1" + "@next/swc-win32-ia32-msvc" "14.1.1" + "@next/swc-win32-x64-msvc" "14.1.1" node-domexception@^1.0.0: version "1.0.0" @@ -1413,14 +1427,14 @@ picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3: resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== -postcss@8.4.5: - version "8.4.5" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.5.tgz#bae665764dfd4c6fcc24dc0fdf7e7aa00cc77f95" - integrity sha512-jBDboWM8qpaqwkMwItqTQTiFikhs/67OYVvblFFTM7MrZjt6yMKd6r2kgXizEbTTljacm4NldIlZnhbjr84QYg== +postcss@8.4.31: + version "8.4.31" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.31.tgz#92b451050a9f914da6755af352bdc0192508656d" + integrity sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ== dependencies: - nanoid "^3.1.30" + nanoid "^3.3.6" picocolors "^1.0.0" - source-map-js "^1.0.1" + source-map-js "^1.0.2" prelude-ls@^1.2.1: version "1.2.1" @@ -1594,11 +1608,21 @@ slash@^3.0.0: resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== -"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.1: +"source-map-js@>=0.6.2 <2.0.0": version "1.0.2" resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== +source-map-js@^1.0.2: + version "1.2.0" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.0.tgz#16b809c162517b5b8c3e7dcd315a2a5c2612b2af" + integrity sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg== + +streamsearch@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764" + integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg== + string.prototype.matchall@^4.0.6: version "4.0.6" resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.6.tgz#5abb5dabc94c7b0ea2380f65ba610b3a544b15fa" @@ -1646,10 +1670,12 @@ strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== -styled-jsx@5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/styled-jsx/-/styled-jsx-5.0.0.tgz#816b4b92e07b1786c6b7111821750e0ba4d26e77" - integrity sha512-qUqsWoBquEdERe10EW8vLp3jT25s/ssG1/qX5gZ4wu15OZpmSMFI2v+fWlRhLfykA5rFtlJ1ME8A8pm/peV4WA== +styled-jsx@5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/styled-jsx/-/styled-jsx-5.1.1.tgz#839a1c3aaacc4e735fed0781b8619ea5d0009d1f" + integrity sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw== + dependencies: + client-only "0.0.1" supports-color@^7.1.0: version "7.2.0" @@ -1690,6 +1716,11 @@ tslib@^1.8.1: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== +tslib@^2.4.0: + version "2.6.3" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.3.tgz#0438f810ad7a9edcde7a241c3d80db693c8cbfe0" + integrity sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ== + tsutils@^3.21.0: version "3.21.0" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" @@ -1709,10 +1740,10 @@ type-fest@^0.20.2: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== -typescript@5.2.2: - version "5.2.2" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.2.2.tgz#5ebb5e5a5b75f085f22bc3f8460fba308310fa78" - integrity sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w== +typescript@5.5.2: + version "5.5.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.2.tgz#c26f023cb0054e657ce04f72583ea2d85f8d0507" + integrity sha512-NcRtPEOsPFFWjobJEtfihkLCZCXZt/os3zf8nTxjVH3RvTSxjrCamJpbExGvYOF+tFHc3pA65qpdwPbzjohhew== unbox-primitive@^1.0.1: version "1.0.1" @@ -1731,13 +1762,6 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" -use-subscription@1.5.1: - version "1.5.1" - resolved "https://registry.yarnpkg.com/use-subscription/-/use-subscription-1.5.1.tgz#73501107f02fad84c6dd57965beb0b75c68c42d1" - integrity sha512-Xv2a1P/yReAjAbhylMfFplFKj9GssgTwN7RlcTxBujFQcloStWNDQdc4g4NRWH9xS4i/FDk04vQBptAXoF3VcA== - dependencies: - object-assign "^4.1.1" - v8-compile-cache@^2.0.3: version "2.3.0" resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" diff --git a/hosting/couchdb/Dockerfile b/hosting/couchdb/Dockerfile index ca72153e78..b95fa348f8 100644 --- a/hosting/couchdb/Dockerfile +++ b/hosting/couchdb/Dockerfile @@ -96,10 +96,13 @@ EXPOSE 5984 4369 9100 CMD ["/opt/couchdb/bin/couchdb"] FROM base as runner +ARG TARGETARCH +ENV TARGETARCH $TARGETARCH ENV COUCHDB_USER admin ENV COUCHDB_PASSWORD admin EXPOSE 5984 +EXPOSE 4984 RUN apt-get update && apt-get install -y --no-install-recommends software-properties-common wget unzip curl && \ wget -O - https://packages.adoptium.net/artifactory/api/gpg/key/public | apt-key add - && \ @@ -125,7 +128,12 @@ ADD clouseau/log4j.properties clouseau/clouseau.ini ./ WORKDIR /opt/couchdb ADD couch/vm.args couch/local.ini ./etc/ +# setup SQS +WORKDIR /opt/sqs +ADD sqs ./ +RUN chmod +x ./install.sh && ./install.sh + WORKDIR / ADD runner.sh ./bbcouch-runner.sh -RUN chmod +x ./bbcouch-runner.sh /opt/clouseau/bin/clouseau -CMD ["./bbcouch-runner.sh"] \ No newline at end of file +RUN chmod +x ./bbcouch-runner.sh /opt/clouseau/bin/clouseau /opt/sqs/sqs +CMD ["./bbcouch-runner.sh"] diff --git a/hosting/couchdb/Dockerfile.v2 b/hosting/couchdb/Dockerfile.v2 deleted file mode 100644 index 126742cadb..0000000000 --- a/hosting/couchdb/Dockerfile.v2 +++ /dev/null @@ -1,139 +0,0 @@ -# Modified from https://github.com/apache/couchdb-docker/blob/main/3.3.3/Dockerfile -# -# Everything in this `base` image is adapted from the official `couchdb` image's -# Dockerfile. Only modifications related to upgrading from Debian bullseye to -# bookworm have been included. The `runner` image contains Budibase's -# customisations to the image, e.g. adding Clouseau. -FROM node:20-slim AS base - -# Add CouchDB user account to make sure the IDs are assigned consistently -RUN groupadd -g 5984 -r couchdb && useradd -u 5984 -d /opt/couchdb -g couchdb couchdb - -# be sure GPG and apt-transport-https are available and functional -RUN set -ex; \ - apt-get update; \ - apt-get install -y --no-install-recommends \ - apt-transport-https \ - ca-certificates \ - dirmngr \ - gnupg \ - ; \ - rm -rf /var/lib/apt/lists/* - -# grab tini for signal handling and zombie reaping -# see https://github.com/apache/couchdb-docker/pull/28#discussion_r141112407 -RUN set -eux; \ - apt-get update; \ - apt-get install -y --no-install-recommends tini; \ - rm -rf /var/lib/apt/lists/*; \ - tini --version - -# http://docs.couchdb.org/en/latest/install/unix.html#installing-the-apache-couchdb-packages -ENV GPG_COUCH_KEY \ -# gpg: rsa8192 205-01-19 The Apache Software Foundation (Package repository signing key) - 390EF70BB1EA12B2773962950EE62FB37A00258D -RUN set -eux; \ - apt-get update; \ - apt-get install -y curl; \ - export GNUPGHOME="$(mktemp -d)"; \ - curl -fL -o keys.asc https://couchdb.apache.org/repo/keys.asc; \ - gpg --batch --import keys.asc; \ - gpg --batch --export "${GPG_COUCH_KEY}" > /usr/share/keyrings/couchdb-archive-keyring.gpg; \ - command -v gpgconf && gpgconf --kill all || :; \ - rm -rf "$GNUPGHOME"; \ - apt-key list; \ - apt purge -y --autoremove curl; \ - rm -rf /var/lib/apt/lists/* - -ENV COUCHDB_VERSION 3.3.3 - -RUN . /etc/os-release; \ - echo "deb [signed-by=/usr/share/keyrings/couchdb-archive-keyring.gpg] https://apache.jfrog.io/artifactory/couchdb-deb/ ${VERSION_CODENAME} main" | \ - tee /etc/apt/sources.list.d/couchdb.list >/dev/null - -# https://github.com/apache/couchdb-pkg/blob/master/debian/README.Debian -RUN set -eux; \ - apt-get update; \ - \ - echo "couchdb couchdb/mode select none" | debconf-set-selections; \ -# we DO want recommends this time - DEBIAN_FRONTEND=noninteractive apt-get install -y --allow-downgrades --allow-remove-essential --allow-change-held-packages \ - couchdb="$COUCHDB_VERSION"~bookworm \ - ; \ -# Undo symlinks to /var/log and /var/lib - rmdir /var/lib/couchdb /var/log/couchdb; \ - rm /opt/couchdb/data /opt/couchdb/var/log; \ - mkdir -p /opt/couchdb/data /opt/couchdb/var/log; \ - chown couchdb:couchdb /opt/couchdb/data /opt/couchdb/var/log; \ - chmod 777 /opt/couchdb/data /opt/couchdb/var/log; \ -# Remove file that sets logging to a file - rm /opt/couchdb/etc/default.d/10-filelog.ini; \ -# Check we own everything in /opt/couchdb. Matches the command in dockerfile_entrypoint.sh - find /opt/couchdb \! \( -user couchdb -group couchdb \) -exec chown -f couchdb:couchdb '{}' +; \ -# Setup directories and permissions for config. Technically these could be 555 and 444 respectively -# but we keep them as 755 and 644 for consistency with CouchDB defaults and the dockerfile_entrypoint.sh. - find /opt/couchdb/etc -type d ! -perm 0755 -exec chmod -f 0755 '{}' +; \ - find /opt/couchdb/etc -type f ! -perm 0644 -exec chmod -f 0644 '{}' +; \ -# only local.d needs to be writable for the docker_entrypoint.sh - chmod -f 0777 /opt/couchdb/etc/local.d; \ -# apt clean-up - rm -rf /var/lib/apt/lists/*; - -# Add configuration -COPY --chown=couchdb:couchdb couch/10-docker-default.ini /opt/couchdb/etc/default.d/ -# COPY --chown=couchdb:couchdb vm.args /opt/couchdb/etc/ - -COPY docker-entrypoint.sh /usr/local/bin -RUN ln -s usr/local/bin/docker-entrypoint.sh /docker-entrypoint.sh # backwards compat -ENTRYPOINT ["tini", "--", "/docker-entrypoint.sh"] - -VOLUME /opt/couchdb/data - -# 5984: Main CouchDB endpoint -# 4369: Erlang portmap daemon (epmd) -# 9100: CouchDB cluster communication port -EXPOSE 5984 4369 9100 -CMD ["/opt/couchdb/bin/couchdb"] - -FROM base as runner -ARG TARGETARCH -ENV TARGETARCH $TARGETARCH - -ENV COUCHDB_USER admin -ENV COUCHDB_PASSWORD admin -EXPOSE 5984 -EXPOSE 4984 - -RUN apt-get update && apt-get install -y --no-install-recommends software-properties-common wget unzip curl && \ - wget -O - https://packages.adoptium.net/artifactory/api/gpg/key/public | apt-key add - && \ - apt-add-repository 'deb http://security.debian.org/debian-security bookworm-security/updates main' && \ - apt-add-repository 'deb http://archive.debian.org/debian stretch-backports main' && \ - apt-add-repository 'deb https://packages.adoptium.net/artifactory/deb bookworm main' && \ - apt-get update && apt-get install -y --no-install-recommends temurin-8-jdk && \ - rm -rf /var/lib/apt/lists/ - -# setup clouseau -WORKDIR / -RUN wget https://github.com/cloudant-labs/clouseau/releases/download/2.21.0/clouseau-2.21.0-dist.zip && \ - unzip clouseau-2.21.0-dist.zip && \ - mv clouseau-2.21.0 /opt/clouseau && \ - rm clouseau-2.21.0-dist.zip - -WORKDIR /opt/clouseau -RUN mkdir ./bin -ADD clouseau/clouseau ./bin/ -ADD clouseau/log4j.properties clouseau/clouseau.ini ./ - -# setup CouchDB -WORKDIR /opt/couchdb -ADD couch/vm.args couch/local.ini ./etc/ - -# setup SQS -WORKDIR /opt/sqs -ADD sqs ./ -RUN chmod +x ./install.sh && ./install.sh - -WORKDIR / -ADD runner.v2.sh ./bbcouch-runner.sh -RUN chmod +x ./bbcouch-runner.sh /opt/clouseau/bin/clouseau /opt/sqs/sqs -CMD ["./bbcouch-runner.sh"] diff --git a/hosting/couchdb/runner.sh b/hosting/couchdb/runner.sh index aaadee6b43..f8cbe49b8f 100644 --- a/hosting/couchdb/runner.sh +++ b/hosting/couchdb/runner.sh @@ -70,9 +70,12 @@ sed -i "s#COUCHDB_ERLANG_COOKIE#${COUCHDB_ERLANG_COOKIE}#g" /opt/clouseau/clouse /opt/clouseau/bin/clouseau > /dev/stdout 2>&1 & # Start CouchDB. -/docker-entrypoint.sh /opt/couchdb/bin/couchdb & +/docker-entrypoint.sh /opt/couchdb/bin/couchdb > /dev/stdout 2>&1 & -# Wati for CouchDB to start up. +# Start SQS. Use 127.0.0.1 instead of localhost to avoid IPv6 issues. +/opt/sqs/sqs --server "http://127.0.0.1:5984" --data-dir ${DATA_DIR}/sqs --bind-address=0.0.0.0 > /dev/stdout 2>&1 & + +# Wait for CouchDB to start up. while [[ $(curl -s -w "%{http_code}\n" http://localhost:5984/_up -o /dev/null) -ne 200 ]]; do echo 'Waiting for CouchDB to start...'; sleep 5; @@ -82,4 +85,4 @@ done # function correctly, so we create them here. curl -X PUT -u "${COUCHDB_USER}:${COUCHDB_PASSWORD}" http://localhost:5984/_users curl -X PUT -u "${COUCHDB_USER}:${COUCHDB_PASSWORD}" http://localhost:5984/_replicator -sleep infinity \ No newline at end of file +sleep infinity diff --git a/hosting/couchdb/runner.v2.sh b/hosting/couchdb/runner.v2.sh deleted file mode 100644 index f8cbe49b8f..0000000000 --- a/hosting/couchdb/runner.v2.sh +++ /dev/null @@ -1,88 +0,0 @@ -#!/bin/bash - -DATA_DIR=${DATA_DIR:-/data} -COUCHDB_ERLANG_COOKIE=${COUCHDB_ERLANG_COOKIE:-B9CFC32C-3458-4A86-8448-B3C753991CA7} - -mkdir -p ${DATA_DIR} -mkdir -p ${DATA_DIR}/couch/{dbs,views} -mkdir -p ${DATA_DIR}/search -chown -R couchdb:couchdb ${DATA_DIR}/couch - -echo ${TARGETBUILD} > /buildtarget.txt -if [[ "${TARGETBUILD}" = "aas" ]]; then - # Azure AppService uses /home for persistent data & SSH on port 2222 - DATA_DIR="${DATA_DIR:-/home}" - WEBSITES_ENABLE_APP_SERVICE_STORAGE=true - mkdir -p $DATA_DIR/{search,minio,couch} - mkdir -p $DATA_DIR/couch/{dbs,views} - chown -R couchdb:couchdb $DATA_DIR/couch/ - apt update - apt-get install -y openssh-server - echo "root:Docker!" | chpasswd - mkdir -p /tmp - chmod +x /tmp/ssh_setup.sh \ - && (sleep 1;/tmp/ssh_setup.sh 2>&1 > /dev/null) - cp /etc/sshd_config /etc/ssh/sshd_config - /etc/init.d/ssh restart - sed -i "s#DATA_DIR#/home#g" /opt/clouseau/clouseau.ini - sed -i "s#DATA_DIR#/home#g" /opt/couchdb/etc/local.ini -elif [[ "${TARGETBUILD}" = "single" ]]; then - # In the single image build, the Dockerfile specifies /data as a volume - # mount, so we use that for all persistent data. - sed -i "s#DATA_DIR#/data#g" /opt/clouseau/clouseau.ini - sed -i "s#DATA_DIR#/data#g" /opt/couchdb/etc/local.ini -elif [[ "${TARGETBUILD}" = "docker-compose" ]]; then - # We remove the database_dir and view_index_dir settings from the local.ini - # in docker-compose because it will default to /opt/couchdb/data which is what - # our docker-compose was using prior to us switching to using our own CouchDB - # image. - sed -i "s#^database_dir.*\$##g" /opt/couchdb/etc/local.ini - sed -i "s#^view_index_dir.*\$##g" /opt/couchdb/etc/local.ini - sed -i "s#^dir=.*\$#dir=/opt/couchdb/data#g" /opt/clouseau/clouseau.ini -elif [[ -n $KUBERNETES_SERVICE_HOST ]]; then - # In Kubernetes the directory /opt/couchdb/data has a persistent volume - # mount for storing database data. - sed -i "s#^dir=.*\$#dir=/opt/couchdb/data#g" /opt/clouseau/clouseau.ini - - # We remove the database_dir and view_index_dir settings from the local.ini - # in Kubernetes because it will default to /opt/couchdb/data which is what - # our Helm chart was using prior to us switching to using our own CouchDB - # image. - sed -i "s#^database_dir.*\$##g" /opt/couchdb/etc/local.ini - sed -i "s#^view_index_dir.*\$##g" /opt/couchdb/etc/local.ini - - # We remove the -name setting from the vm.args file in Kubernetes because - # it will default to the pod FQDN, which is what's required for clustering - # to work. - sed -i "s/^-name .*$//g" /opt/couchdb/etc/vm.args -else - # For all other builds, we use /data for persistent data. - sed -i "s#DATA_DIR#/data#g" /opt/clouseau/clouseau.ini - sed -i "s#DATA_DIR#/data#g" /opt/couchdb/etc/local.ini -fi - -sed -i "s#COUCHDB_ERLANG_COOKIE#${COUCHDB_ERLANG_COOKIE}#g" /opt/couchdb/etc/vm.args -sed -i "s#COUCHDB_ERLANG_COOKIE#${COUCHDB_ERLANG_COOKIE}#g" /opt/clouseau/clouseau.ini - -# Start Clouseau. Budibase won't function correctly without Clouseau running, it -# powers the search API endpoints which are used to do all sorts, including -# populating app grids. -/opt/clouseau/bin/clouseau > /dev/stdout 2>&1 & - -# Start CouchDB. -/docker-entrypoint.sh /opt/couchdb/bin/couchdb > /dev/stdout 2>&1 & - -# Start SQS. Use 127.0.0.1 instead of localhost to avoid IPv6 issues. -/opt/sqs/sqs --server "http://127.0.0.1:5984" --data-dir ${DATA_DIR}/sqs --bind-address=0.0.0.0 > /dev/stdout 2>&1 & - -# Wait for CouchDB to start up. -while [[ $(curl -s -w "%{http_code}\n" http://localhost:5984/_up -o /dev/null) -ne 200 ]]; do - echo 'Waiting for CouchDB to start...'; - sleep 5; -done - -# CouchDB needs the `_users` and `_replicator` databases to exist before it will -# function correctly, so we create them here. -curl -X PUT -u "${COUCHDB_USER}:${COUCHDB_PASSWORD}" http://localhost:5984/_users -curl -X PUT -u "${COUCHDB_USER}:${COUCHDB_PASSWORD}" http://localhost:5984/_replicator -sleep infinity diff --git a/lerna.json b/lerna.json index 10bb67fd27..c3419a3f87 100644 --- a/lerna.json +++ b/lerna.json @@ -1,6 +1,6 @@ { "$schema": "node_modules/lerna/schemas/lerna-schema.json", - "version": "2.29.5", + "version": "2.29.13", "npmClient": "yarn", "packages": [ "packages/*", @@ -22,4 +22,4 @@ "loadEnvFiles": false } } -} +} \ No newline at end of file diff --git a/package.json b/package.json index 7e5ce25923..29b87898ac 100644 --- a/package.json +++ b/package.json @@ -33,10 +33,10 @@ "scripts": { "get-past-client-version": "node scripts/getPastClientVersion.js", "setup": "git config submodule.recurse true && git submodule update && node ./hosting/scripts/setup.js && yarn && yarn build && yarn dev", - "build": "NODE_OPTIONS=--max-old-space-size=1500 lerna run build --stream", - "build:apps": "yarn build --scope @budibase/server --scope @budibase/worker", + "build": "DISABLE_V8_COMPILE_CACHE=1 NODE_OPTIONS=--max-old-space-size=1500 lerna run build --stream", + "build:apps": "DISABLE_V8_COMPILE_CACHE=1 yarn build --scope @budibase/server --scope @budibase/worker", + "build:oss": "DISABLE_V8_COMPILE_CACHE=1 NODE_OPTIONS=--max-old-space-size=1500 lerna run build --stream --ignore @budibase/account-portal-server --ignore @budibase/account-portal-ui", "build:cli": "yarn build --scope @budibase/cli", - "build:oss": "NODE_OPTIONS=--max-old-space-size=1500 lerna run build --stream --ignore @budibase/account-portal-server --ignore @budibase/account-portal-ui", "build:account-portal": "NODE_OPTIONS=--max-old-space-size=1500 lerna run build --stream --scope @budibase/account-portal-server --scope @budibase/account-portal-ui", "build:dev": "lerna run --stream prebuild && yarn nx run-many --target=build --output-style=dynamic --watch --preserveWatchOutput", "check:types": "lerna run --concurrency 2 check:types --ignore @budibase/account-portal-server", @@ -77,7 +77,6 @@ "build:docker:single:sqs": "./scripts/build-single-image-sqs.sh", "build:docker:dependencies": "docker build -f hosting/dependencies/Dockerfile -t budibase/dependencies:latest ./hosting", "publish:docker:couch": "docker buildx build --platform linux/arm64,linux/amd64 -f hosting/couchdb/Dockerfile -t budibase/couchdb:latest -t budibase/couchdb:v3.3.3 --push ./hosting/couchdb", - "publish:docker:couch-sqs": "docker buildx build --platform linux/arm64,linux/amd64 -f hosting/couchdb/Dockerfile.v2 -t budibase/couchdb:v3.3.3-sqs --push ./hosting/couchdb", "publish:docker:dependencies": "docker buildx build --platform linux/arm64,linux/amd64 -f hosting/dependencies/Dockerfile -t budibase/dependencies:latest -t budibase/dependencies:v3.2.1 --push ./hosting", "release:helm": "node scripts/releaseHelmChart", "env:multi:enable": "lerna run --stream env:multi:enable", diff --git a/packages/account-portal b/packages/account-portal index ff16525b73..b03e584e46 160000 --- a/packages/account-portal +++ b/packages/account-portal @@ -1 +1 @@ -Subproject commit ff16525b73c5751d344f5c161a682609c0a993f2 +Subproject commit b03e584e465f620b49a1b688ff4afc973e6c0758 diff --git a/packages/backend-core/package.json b/packages/backend-core/package.json index 88b970884c..bf5215a724 100644 --- a/packages/backend-core/package.json +++ b/packages/backend-core/package.json @@ -22,10 +22,9 @@ }, "dependencies": { "@budibase/nano": "10.1.5", - "@budibase/pouchdb-replication-stream": "1.2.10", + "@budibase/pouchdb-replication-stream": "1.2.11", "@budibase/shared-core": "0.0.0", "@budibase/types": "0.0.0", - "@govtechsg/passport-openidconnect": "^1.0.2", "aws-cloudfront-sign": "3.0.2", "aws-sdk": "2.1030.0", "bcrypt": "5.1.0", diff --git a/packages/backend-core/src/db/couch/DatabaseImpl.ts b/packages/backend-core/src/db/couch/DatabaseImpl.ts index 4db63ad695..274c09438d 100644 --- a/packages/backend-core/src/db/couch/DatabaseImpl.ts +++ b/packages/backend-core/src/db/couch/DatabaseImpl.ts @@ -80,6 +80,11 @@ export function DatabaseWithConnection( connection: string, opts?: DatabaseOpts ) { + if (!dbName || !connection) { + throw new Error( + "Unable to create database without database name or connection" + ) + } const db = new DatabaseImpl(dbName, opts, connection) return new DDInstrumentedDatabase(db) } diff --git a/packages/backend-core/src/db/couch/connections.ts b/packages/backend-core/src/db/couch/connections.ts index 8dbbe34e3a..454715e2cb 100644 --- a/packages/backend-core/src/db/couch/connections.ts +++ b/packages/backend-core/src/db/couch/connections.ts @@ -1,6 +1,7 @@ import env from "../../environment" export const getCouchInfo = (connection?: string) => { + // clean out any auth credentials const urlInfo = getUrlInfo(connection) let username let password @@ -23,9 +24,16 @@ export const getCouchInfo = (connection?: string) => { throw new Error("CouchDB password not set") } const authCookie = Buffer.from(`${username}:${password}`).toString("base64") + let sqlUrl = env.COUCH_DB_SQL_URL + if (!sqlUrl && urlInfo.url) { + const parsed = new URL(urlInfo.url) + // attempt to connect on default port + sqlUrl = urlInfo.url.replace(parsed.port, "4984") + } return { url: urlInfo.url!, - sqlUrl: env.COUCH_DB_SQL_URL, + // clean out any auth credentials + sqlUrl: getUrlInfo(sqlUrl).url, auth: { username: username, password: password, diff --git a/packages/backend-core/src/db/tests/connections.spec.ts b/packages/backend-core/src/db/tests/connections.spec.ts new file mode 100644 index 0000000000..2130467292 --- /dev/null +++ b/packages/backend-core/src/db/tests/connections.spec.ts @@ -0,0 +1,22 @@ +import env from "../../environment" +import { getCouchInfo } from "../couch" + +const MAIN_COUCH_URL = "http://user:test@localhost:5984" + +describe("connections", () => { + beforeAll(() => { + env._set("COUCH_DB_SQL_URL", "https://user:test@localhost:4984") + }) + + it("should strip URL credentials", () => { + const response = getCouchInfo(MAIN_COUCH_URL) + expect(response.url).toBe("http://localhost:5984") + expect(response.sqlUrl).toBe("https://localhost:4984") + }) + + it("should return separate auth credentials", () => { + const response = getCouchInfo(MAIN_COUCH_URL) + expect(response.auth.username).toBe("user") + expect(response.auth.password).toBe("test") + }) +}) diff --git a/packages/backend-core/src/environment.ts b/packages/backend-core/src/environment.ts index e58660a889..e06d51f918 100644 --- a/packages/backend-core/src/environment.ts +++ b/packages/backend-core/src/environment.ts @@ -200,8 +200,28 @@ const environment = { }, ROLLING_LOG_MAX_SIZE: process.env.ROLLING_LOG_MAX_SIZE || "10M", DISABLE_SCIM_CALLS: process.env.DISABLE_SCIM_CALLS, + BB_ADMIN_USER_EMAIL: process.env.BB_ADMIN_USER_EMAIL, + BB_ADMIN_USER_PASSWORD: process.env.BB_ADMIN_USER_PASSWORD, + OPENAI_API_KEY: process.env.OPENAI_API_KEY, } +type EnvironmentKey = keyof typeof environment +export const SECRETS: EnvironmentKey[] = [ + "API_ENCRYPTION_KEY", + "BB_ADMIN_USER_PASSWORD", + "COUCH_DB_PASSWORD", + "COUCH_DB_SQL_URL", + "COUCH_DB_URL", + "GOOGLE_CLIENT_SECRET", + "INTERNAL_API_KEY_FALLBACK", + "INTERNAL_API_KEY", + "JWT_SECRET", + "MINIO_ACCESS_KEY", + "MINIO_SECRET_KEY", + "OPENAI_API_KEY", + "REDIS_PASSWORD", +] + // clean up any environment variable edge cases for (let [key, value] of Object.entries(environment)) { // handle the edge case of "0" to disable an environment variable diff --git a/packages/backend-core/src/middleware/errorHandling.ts b/packages/backend-core/src/middleware/errorHandling.ts index 08f9f3214d..6ceda9cd3a 100644 --- a/packages/backend-core/src/middleware/errorHandling.ts +++ b/packages/backend-core/src/middleware/errorHandling.ts @@ -1,6 +1,7 @@ import { APIError } from "@budibase/types" import * as errors from "../errors" import environment from "../environment" +import { stringContainsSecret } from "../security/secrets" export async function errorHandling(ctx: any, next: any) { try { @@ -17,11 +18,19 @@ export async function errorHandling(ctx: any, next: any) { let error: APIError = { message: err.message, - status: status, + status, validationErrors: err.validation, error: errors.getPublicError(err), } + if (stringContainsSecret(JSON.stringify(error))) { + error = { + message: "Unexpected error", + status, + error: "Unexpected error", + } + } + if (environment.isTest() && ctx.headers["x-budibase-include-stacktrace"]) { // @ts-ignore error.stack = err.stack diff --git a/packages/backend-core/src/security/secrets.ts b/packages/backend-core/src/security/secrets.ts new file mode 100644 index 0000000000..65bc33a1dc --- /dev/null +++ b/packages/backend-core/src/security/secrets.ts @@ -0,0 +1,20 @@ +import environment, { SECRETS } from "../environment" + +export function stringContainsSecret(str: string) { + if (str.includes("-----BEGIN PRIVATE KEY-----")) { + return true + } + + for (const key of SECRETS) { + const value = environment[key] + if (typeof value !== "string" || value === "") { + continue + } + + if (str.includes(value)) { + return true + } + } + + return false +} diff --git a/packages/backend-core/src/security/tests/secrets.spec.ts b/packages/backend-core/src/security/tests/secrets.spec.ts new file mode 100644 index 0000000000..19bf174973 --- /dev/null +++ b/packages/backend-core/src/security/tests/secrets.spec.ts @@ -0,0 +1,35 @@ +import { randomUUID } from "crypto" +import environment, { SECRETS } from "../../environment" +import { stringContainsSecret } from "../secrets" + +describe("secrets", () => { + describe("stringContainsSecret", () => { + it.each(SECRETS)("detects that a string contains a secret in: %s", key => { + const needle = randomUUID() + const haystack = `this is a secret: ${needle}` + const old = environment[key] + environment._set(key, needle) + + try { + expect(stringContainsSecret(haystack)).toBe(true) + } finally { + environment._set(key, old) + } + }) + + it.each(SECRETS)( + "detects that a string does not contain a secret in: %s", + key => { + const needle = randomUUID() + const haystack = `this does not contain a secret` + const old = environment[key] + environment._set(key, needle) + try { + expect(stringContainsSecret(haystack)).toBe(false) + } finally { + environment._set(key, old) + } + } + ) + }) +}) diff --git a/packages/backend-core/src/users/db.ts b/packages/backend-core/src/users/db.ts index 37547573bd..4865ebb5bc 100644 --- a/packages/backend-core/src/users/db.ts +++ b/packages/backend-core/src/users/db.ts @@ -221,7 +221,7 @@ export class UserDB { const tenantId = getTenantId() const db = getGlobalDB() - let { email, _id, userGroups = [], roles } = user + const { email, _id, userGroups = [], roles } = user if (!email && !_id) { throw new Error("_id or email is required") @@ -231,11 +231,10 @@ export class UserDB { if (_id) { // try to get existing user from db try { - dbUser = (await db.get(_id)) as User - if (email && dbUser.email !== email) { - throw "Email address cannot be changed" + dbUser = await usersCore.getById(_id) + if (email && dbUser.email !== email && !opts.allowChangingEmail) { + throw new Error("Email address cannot be changed") } - email = dbUser.email } catch (e: any) { if (e.status === 404) { // do nothing, save this new user with the id specified - required for SSO auth @@ -271,13 +270,13 @@ export class UserDB { // make sure we set the _id field for a new user // Also if this is a new user, associate groups with them - let groupPromises = [] + const groupPromises = [] if (!_id) { - _id = builtUser._id! - if (userGroups.length > 0) { for (let groupId of userGroups) { - groupPromises.push(UserDB.groups.addUsers(groupId, [_id!])) + groupPromises.push( + UserDB.groups.addUsers(groupId, [builtUser._id!]) + ) } } } @@ -288,6 +287,11 @@ export class UserDB { builtUser._rev = response.rev await eventHelpers.handleSaveEvents(builtUser, dbUser) + if (dbUser && builtUser.email !== dbUser.email) { + // Remove the plaform email reference if the email changed + await platform.users.removeUser({ email: dbUser.email } as User) + } + await platform.users.addUser( tenantId, builtUser._id!, diff --git a/packages/backend-core/src/users/test/db.spec.ts b/packages/backend-core/src/users/test/db.spec.ts new file mode 100644 index 0000000000..3e29d6673c --- /dev/null +++ b/packages/backend-core/src/users/test/db.spec.ts @@ -0,0 +1,188 @@ +import { User, UserStatus } from "@budibase/types" +import { DBTestConfiguration, generator, structures } from "../../../tests" +import { UserDB } from "../db" +import { searchExistingEmails } from "../lookup" + +const db = UserDB + +const config = new DBTestConfiguration() + +const quotas = { + addUsers: jest + .fn() + .mockImplementation( + (_change: number, _creatorsChange: number, cb?: () => Promise) => + cb && cb() + ), + removeUsers: jest + .fn() + .mockImplementation( + (_change: number, _creatorsChange: number, cb?: () => Promise) => + cb && cb() + ), +} +const groups = { + addUsers: jest.fn(), + getBulk: jest.fn(), + getGroupBuilderAppIds: jest.fn(), +} +const features = { isSSOEnforced: jest.fn(), isAppBuildersEnabled: jest.fn() } + +describe("UserDB", () => { + beforeAll(() => { + db.init(quotas, groups, features) + }) + + describe("save", () => { + describe("create", () => { + it("creating a new user will persist it", async () => { + const email = generator.email({}) + const user: User = structures.users.user({ + email, + tenantId: config.getTenantId(), + }) + + await config.doInTenant(async () => { + const saveUserResponse = await db.save(user) + + const persistedUser = await db.getUserByEmail(email) + expect(persistedUser).toEqual({ + ...user, + _id: saveUserResponse._id, + _rev: expect.stringMatching(/^1-\w+/), + password: expect.not.stringMatching(user.password!), + status: UserStatus.ACTIVE, + createdAt: Date.now(), + updatedAt: new Date().toISOString(), + }) + }) + }) + + it("the same email cannot be used twice in the same tenant", async () => { + const email = generator.email({}) + const user: User = structures.users.user({ + email, + tenantId: config.getTenantId(), + }) + + await config.doInTenant(() => db.save(user)) + + await config.doInTenant(() => + expect(db.save(user)).rejects.toThrow( + `Email already in use: '${email}'` + ) + ) + }) + + it("the same email cannot be used twice in different tenants", async () => { + const email = generator.email({}) + const user: User = structures.users.user({ + email, + tenantId: config.getTenantId(), + }) + + await config.doInTenant(() => db.save(user)) + + config.newTenant() + await config.doInTenant(() => + expect(db.save(user)).rejects.toThrow( + `Email already in use: '${email}'` + ) + ) + }) + }) + + describe("update", () => { + let user: User + + beforeEach(async () => { + user = await config.doInTenant(() => + db.save( + structures.users.user({ + email: generator.email({}), + tenantId: config.getTenantId(), + }) + ) + ) + }) + + it("can update user properties", async () => { + await config.doInTenant(async () => { + const updatedName = generator.first() + user.firstName = updatedName + + await db.save(user) + + const persistedUser = await db.getUserByEmail(user.email) + expect(persistedUser).toEqual( + expect.objectContaining({ + _id: user._id, + email: user.email, + firstName: updatedName, + lastName: user.lastName, + }) + ) + }) + }) + + it("email cannot be updated by default", async () => { + await config.doInTenant(async () => { + await expect( + db.save({ ...user, email: generator.email({}) }) + ).rejects.toThrow("Email address cannot be changed") + }) + }) + + it("email can be updated if specified", async () => { + await config.doInTenant(async () => { + const newEmail = generator.email({}) + + await db.save( + { ...user, email: newEmail }, + { allowChangingEmail: true } + ) + + const persistedUser = await db.getUserByEmail(newEmail) + expect(persistedUser).toEqual( + expect.objectContaining({ + _id: user._id, + email: newEmail, + lastName: user.lastName, + _rev: expect.stringMatching(/^2-\w+/), + }) + ) + }) + }) + + it("updating emails frees previous emails", async () => { + await config.doInTenant(async () => { + const previousEmail = user.email + const newEmail = generator.email({}) + expect(await searchExistingEmails([previousEmail, newEmail])).toEqual( + [previousEmail] + ) + + await db.save( + { ...user, email: newEmail }, + { allowChangingEmail: true } + ) + + expect(await searchExistingEmails([previousEmail, newEmail])).toEqual( + [newEmail] + ) + + await db.save( + structures.users.user({ + email: previousEmail, + tenantId: config.getTenantId(), + }) + ) + + expect(await searchExistingEmails([previousEmail, newEmail])).toEqual( + [previousEmail, newEmail] + ) + }) + }) + }) + }) +}) diff --git a/packages/bbui/src/Tooltip/TooltipWrapper.svelte b/packages/bbui/src/Tooltip/TooltipWrapper.svelte index 4c20cb54a0..8d12b88086 100644 --- a/packages/bbui/src/Tooltip/TooltipWrapper.svelte +++ b/packages/bbui/src/Tooltip/TooltipWrapper.svelte @@ -1,33 +1,25 @@ -
{#if tooltip}
-
(showTooltip = true)} - on:mouseleave={() => (showTooltip = false)} - on:focus - > - -
- {#if showTooltip} -
- + +
+
- {/if} +
{/if}
@@ -44,14 +36,6 @@ margin-left: 5px; margin-right: 5px; } - .tooltip { - position: absolute; - display: flex; - justify-content: center; - top: 15px; - z-index: 200; - width: 160px; - } .icon { transform: scale(0.75); } diff --git a/packages/builder/package.json b/packages/builder/package.json index a00936bdca..f44c2ea549 100644 --- a/packages/builder/package.json +++ b/packages/builder/package.json @@ -74,7 +74,7 @@ "lodash": "4.17.21", "posthog-js": "^1.118.0", "remixicon": "2.5.0", - "sanitize-html": "^2.7.0", + "sanitize-html": "^2.13.0", "shortid": "2.2.15", "svelte-dnd-action": "^0.9.8", "svelte-loading-spinners": "^0.1.1", diff --git a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowChart.svelte b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowChart.svelte index d68e57ca36..f79b36b1ca 100644 --- a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowChart.svelte +++ b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowChart.svelte @@ -112,7 +112,7 @@ This action cannot be undone. - + @@ -148,7 +148,6 @@ .header.scrolling { background: var(--background); border-bottom: var(--border-light); - border-left: var(--border-light); z-index: 1; } diff --git a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/TestDataModal.svelte b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/TestDataModal.svelte index d212300cdf..7d223299c7 100644 --- a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/TestDataModal.svelte +++ b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/TestDataModal.svelte @@ -8,11 +8,63 @@ import { automationStore, selectedAutomation } from "stores/builder" import AutomationBlockSetup from "../../SetupPanel/AutomationBlockSetup.svelte" import { cloneDeep } from "lodash/fp" + import { memo } from "@budibase/frontend-core" + import { AutomationEventType } from "@budibase/types" let failedParse = null let trigger = {} let schemaProperties = {} + const rowTriggers = [ + AutomationEventType.ROW_DELETE, + AutomationEventType.ROW_UPDATE, + AutomationEventType.ROW_SAVE, + ] + + /** + * Parses the automation test data and ensures it is valid + * @param {object} testData contains all config for the test + * @returns {object} valid testData + * @todo Parse *all* data for each trigger type and relay adequate feedback + */ + const parseTestData = testData => { + const autoTrigger = $selectedAutomation?.definition?.trigger + const { tableId } = autoTrigger?.inputs || {} + + // Ensure the tableId matches the trigger table for row trigger automations + if ( + rowTriggers.includes(autoTrigger?.event) && + testData?.row?.tableId !== tableId + ) { + return { + // Reset Core fields + row: { tableId }, + meta: {}, + id: "", + revision: "", + } + } else { + // Leave the core data as it is + return testData + } + } + + /** + * Before executing a test run, relay if an automation is in a valid state + * @param {object} trigger The automation trigger config + * @returns {boolean} validation status + * @todo Parse *all* trigger types relay adequate feedback + */ + const isTriggerValid = trigger => { + if (rowTriggers.includes(trigger?.event) && !trigger?.inputs?.tableId) { + return false + } + return true + } + + const memoTestData = memo(parseTestData($selectedAutomation.testData)) + $: memoTestData.set(parseTestData($selectedAutomation.testData)) + $: { // clone the trigger so we're not mutating the reference trigger = cloneDeep($selectedAutomation.definition.trigger) @@ -20,34 +72,45 @@ // get the outputs so we can define the fields let schema = Object.entries(trigger.schema?.outputs?.properties || {}) - if (trigger?.event === "app:trigger") { + if (trigger?.event === AutomationEventType.APP_TRIGGER) { schema = [["fields", { customType: "fields" }]] } - schemaProperties = schema } - // check to see if there is existing test data in the store - $: testData = $selectedAutomation.testData || {} - // Check the schema to see if required fields have been entered - $: isError = !trigger.schema.outputs.required.every( - required => testData[required] || required !== "row" - ) + $: isError = + !isTriggerValid(trigger) || + !trigger.schema.outputs.required.every( + required => $memoTestData?.[required] || required !== "row" + ) function parseTestJSON(e) { + let jsonUpdate + try { - const obj = JSON.parse(e.detail) + jsonUpdate = JSON.parse(e.detail) failedParse = null - automationStore.actions.addTestDataToAutomation(obj) } catch (e) { failedParse = "Invalid JSON" + return false } + + if (rowTriggers.includes(trigger?.event)) { + const tableId = trigger?.inputs?.tableId + + // Reset the tableId as it must match the trigger + if (jsonUpdate?.row?.tableId !== tableId) { + jsonUpdate.row.tableId = tableId + } + } + + automationStore.actions.addTestDataToAutomation(jsonUpdate) } const testAutomation = async () => { try { - await automationStore.actions.test($selectedAutomation, testData) + await automationStore.actions.test($selectedAutomation, $memoTestData) $automationStore.showTestPanel = true } catch (error) { notifications.error(error) @@ -85,7 +148,7 @@ {#if selectedValues}
import TableSelector from "./TableSelector.svelte" - import RowSelector from "./RowSelector.svelte" import FieldSelector from "./FieldSelector.svelte" import SchemaSetup from "./SchemaSetup.svelte" + import RowSelector from "./RowSelector.svelte" import { Button, - Input, Select, Label, ActionButton, @@ -15,26 +14,27 @@ Checkbox, DatePicker, DrawerContent, + Helpers, Toggle, - Icon, Divider, } from "@budibase/bbui" import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte" import { automationStore, selectedAutomation, tables } from "stores/builder" import { environment, licensing } from "stores/portal" import WebhookDisplay from "../Shared/WebhookDisplay.svelte" - import DrawerBindableInput from "../../common/bindings/DrawerBindableInput.svelte" - import DrawerBindableSlot from "../../common/bindings/DrawerBindableSlot.svelte" - import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte" + import { + BindingSidePanel, + DrawerBindableSlot, + DrawerBindableInput, + ServerBindingPanel as AutomationBindingPanel, + ModalBindableInput, + } from "components/common/bindings" import CodeEditorModal from "./CodeEditorModal.svelte" - import QuerySelector from "./QuerySelector.svelte" import QueryParamSelector from "./QueryParamSelector.svelte" import AutomationSelector from "./AutomationSelector.svelte" import CronBuilder from "./CronBuilder.svelte" import Editor from "components/integration/QueryEditor.svelte" - import ModalBindableInput from "components/common/bindings/ModalBindableInput.svelte" import CodeEditor from "components/common/CodeEditor/CodeEditor.svelte" - import BindingSidePanel from "components/common/bindings/BindingSidePanel.svelte" import KeyValueBuilder from "components/integration/KeyValueBuilder.svelte" import { BindingHelpers, BindingType } from "components/common/bindings/utils" import { @@ -43,31 +43,57 @@ EditorModes, } from "components/common/CodeEditor" import FilterBuilder from "components/design/settings/controls/FilterEditor/FilterBuilder.svelte" - import { QueryUtils, Utils, search } from "@budibase/frontend-core" + import { QueryUtils, Utils, search, memo } from "@budibase/frontend-core" import { getSchemaForDatasourcePlus, getEnvironmentBindings, } from "dataBinding" import { TriggerStepID, ActionStepID } from "constants/backend/automations" import { onMount } from "svelte" + import { writable } from "svelte/store" import { cloneDeep } from "lodash/fp" + import { + AutomationEventType, + AutomationStepType, + AutomationActionStepId, + } from "@budibase/types" import { FIELDS } from "constants/backend" + import PropField from "./PropField.svelte" export let block export let testData export let schemaProperties export let isTestModal = false + // Stop unnecessary rendering + const memoBlock = memo(block) + + const rowTriggers = [ + TriggerStepID.ROW_UPDATED, + TriggerStepID.ROW_SAVED, + TriggerStepID.ROW_DELETED, + ] + + const rowEvents = [ + AutomationEventType.ROW_DELETE, + AutomationEventType.ROW_SAVE, + AutomationEventType.ROW_UPDATE, + ] + + const rowSteps = [ActionStepID.UPDATE_ROW, ActionStepID.CREATE_ROW] + let webhookModal let drawer let inputData let insertAtPos, getCaretPosition + let stepLayouts = {} + $: memoBlock.set(block) $: filters = lookForFilters(schemaProperties) || [] $: tempFilters = filters $: stepId = block.stepId $: bindings = getAvailableBindings(block, $selectedAutomation?.definition) - $: getInputData(testData, block.inputs) + $: getInputData(testData, $memoBlock.inputs) $: tableId = inputData ? inputData.tableId : null $: table = tableId ? $tables.list.find(table => table._id === inputData.tableId) @@ -81,31 +107,33 @@ { allowLinks: true } ) $: queryLimit = tableId?.includes("datasource") ? "∞" : "1000" - $: isTrigger = block?.type === "TRIGGER" - $: isUpdateRow = stepId === ActionStepID.UPDATE_ROW + $: isTrigger = block?.type === AutomationStepType.TRIGGER $: codeMode = - stepId === "EXECUTE_BASH" ? EditorModes.Handlebars : EditorModes.JS + stepId === AutomationActionStepId.EXECUTE_BASH + ? EditorModes.Handlebars + : EditorModes.JS $: bindingsHelpers = new BindingHelpers(getCaretPosition, insertAtPos, { disableWrapping: true, }) $: editingJs = codeMode === EditorModes.JS - $: requiredProperties = block.schema.inputs.required || [] + $: requiredProperties = isTestModal ? [] : block.schema["inputs"].required + $: stepCompletions = codeMode === EditorModes.Handlebars ? [hbAutocomplete([...bindingsToCompletions(bindings, codeMode)])] : [] - let testDataRowVisibility = {} - const getInputData = (testData, blockInputs) => { // Test data is not cloned for reactivity let newInputData = testData || cloneDeep(blockInputs) // Ensures the app action fields are populated - if (block.event === "app:trigger" && !newInputData?.fields) { + if ( + block.event === AutomationEventType.APP_TRIGGER && + !newInputData?.fields + ) { newInputData = cloneDeep(blockInputs) } - inputData = newInputData setDefaultEnumValues() } @@ -117,15 +145,338 @@ } } } - const onChange = Utils.sequential(async (e, key) => { + + // Store for any UX related data + const stepStore = writable({}) + $: currentStep = $stepStore?.[block.id] + + $: customStepLayouts($memoBlock, schemaProperties, currentStep) + + const customStepLayouts = block => { + if ( + rowSteps.includes(block.stepId) || + (rowTriggers.includes(block.stepId) && isTestModal) + ) { + const schema = schemaProperties.reduce((acc, entry) => { + const [key, val] = entry + acc[key] = val + return acc + }, {}) + + // Optionally build the rev field config when its needed. + const getRevConfig = () => { + const rowRevEntry = schema["revision"] + if (!rowRevEntry) { + return [] + } + const rowRevlabel = getFieldLabel("revision", rowRevEntry) + + return isTestModal + ? [ + { + type: DrawerBindableInput, + title: rowRevlabel, + props: { + panel: AutomationBindingPanel, + value: inputData["revision"], + onChange: e => { + onChange({ ["revision"]: e.detail }) + }, + bindings, + updateOnChange: false, + forceModal: true, + }, + }, + ] + : [] + } + + const getIdConfig = () => { + const rowIdentifier = isTestModal ? "id" : "rowId" + + const rowIdEntry = schema[rowIdentifier] + if (!rowIdEntry) { + return [] + } + + const rowIdlabel = getFieldLabel(rowIdentifier, rowIdEntry) + + return [ + { + type: DrawerBindableInput, + title: rowIdlabel, + props: { + panel: AutomationBindingPanel, + value: inputData[rowIdentifier], + onChange: e => { + onChange({ [rowIdentifier]: e.detail }) + }, + bindings, + updateOnChange: false, + forceModal: true, + }, + }, + ] + } + + // A select to switch from `row` to `oldRow` + const getRowTypeConfig = () => { + if (!isTestModal || block.event !== AutomationEventType.ROW_UPDATE) { + return [] + } + + if (!$stepStore?.[block.id]) { + stepStore.update(state => ({ + ...state, + [block.id]: { + rowType: "row", + }, + })) + } + + return [ + { + type: Select, + tooltip: `You can configure test data for both the updated row and + the old row, if you need it. Just select the one you wish to alter`, + title: "Row data", + props: { + value: $stepStore?.[block.id].rowType, + onChange: e => { + stepStore.update(state => ({ + ...state, + [block.id]: { + rowType: e.detail, + }, + })) + }, + getOptionLabel: type => type.name, + getOptionValue: type => type.id, + options: [ + { + id: "row", + name: "Updated row", + }, + { id: "oldRow", name: "Old row" }, + ], + }, + }, + ] + } + + const getRowSelector = () => { + const baseProps = { + bindings, + isTestModal, + isUpdateRow: block.stepId === ActionStepID.UPDATE_ROW, + } + + if (isTestModal && currentStep?.rowType === "oldRow") { + return [ + { + type: RowSelector, + props: { + row: inputData["oldRow"] || { + tableId: inputData["row"].tableId, + }, + meta: { + fields: inputData["meta"].oldFields || {}, + }, + onChange: e => { + onChange({ + oldRow: e.detail.row, + meta: { + fields: inputData["meta"].fields, + oldFields: e.detail.meta.fields, + }, + }) + }, + ...baseProps, + }, + }, + ] + } + + return [ + { + type: RowSelector, + props: { + row: inputData["row"], + meta: inputData["meta"] || {}, + onChange: e => { + onChange(e.detail) + }, + ...baseProps, + }, + }, + ] + } + + stepLayouts[block.stepId] = { + row: { + schema: schema["row"], + //?layout: RowLayoutStepComponent. + content: [ + { + type: TableSelector, + title: "Table", + props: { + isTrigger, + value: inputData["row"]?.tableId ?? "", + onChange: e => { + const rowKey = $stepStore?.[block.id]?.rowType || "row" + onChange({ + _tableId: e.detail, + meta: {}, + [rowKey]: e.detail + ? { + tableId: e.detail, + } + : {}, + }) + }, + disabled: isTestModal, + }, + }, + ...getIdConfig(), + ...getRevConfig(), + ...getRowTypeConfig(), + { + type: Divider, + props: { + noMargin: true, + }, + }, + ...getRowSelector(), + ], + }, + } + } + } + + /** + * Handler for row trigger automation updates. + @param {object} update - An automation block.inputs update object + @example + onRowTriggerUpdate({ + "tableId" : "ta_bb_employee" + }) + */ + const onRowTriggerUpdate = async update => { + if ( + Object.hasOwn(update, "tableId") && + $selectedAutomation.testData?.row?.tableId !== update.tableId + ) { + try { + const reqSchema = getSchemaForDatasourcePlus(update.tableId, { + searchableSchema: true, + }).schema + + // Parse the block inputs as usual + const updatedAutomation = + await automationStore.actions.processBlockInputs(block, { + schema: reqSchema, + ...update, + }) + + // Save the entire automation and reset the testData + await automationStore.actions.save({ + ...updatedAutomation, + testData: { + // Reset Core fields + row: { tableId: update.tableId }, + oldRow: { tableId: update.tableId }, + meta: {}, + id: "", + revision: "", + }, + }) + + return + } catch (e) { + console.error("Error saving automation", e) + notifications.error("Error saving automation") + } + } + } + + /** + * Handler for App trigger automation updates. + * Ensure updates to the field list are reflected in testData + @param {object} update - An app trigger update object + @example + onAppTriggerUpdate({ + "fields" : {"myField": "123", "myArray": "cat,dog,badger"} + }) + */ + const onAppTriggerUpdate = async update => { + try { + // Parse the block inputs as usual + const updatedAutomation = + await automationStore.actions.processBlockInputs(block, { + schema: {}, + ...update, + }) + + // Exclude default or invalid data from the test data + let updatedFields = {} + for (const key of Object.keys(block?.inputs?.fields || {})) { + if (Object.hasOwn(update.fields, key)) { + if (key !== "") { + updatedFields[key] = updatedAutomation.testData?.fields?.[key] + } + } + } + + // Save the entire automation and reset the testData + await automationStore.actions.save({ + ...updatedAutomation, + testData: { + fields: updatedFields, + }, + }) + } catch (e) { + console.error("Error saving automation", e) + notifications.error("Error saving automation") + } + } + + /** + * Handler for automation block input updates. + @param {object} update - An automation inputs update object + @example + onChange({ + meta: { fields : { "Photo": { useAttachmentBinding: false }} } + row: { "Active": true, "Order Id" : 14, ... } + }) + */ + const onChange = Utils.sequential(async update => { + const request = cloneDeep(update) + + // Process app trigger updates + if (isTrigger && !isTestModal) { + // Row trigger + if (rowEvents.includes(block.event)) { + await onRowTriggerUpdate(request) + return + } + // App trigger + if (block.event === AutomationEventType.APP_TRIGGER) { + await onAppTriggerUpdate(request) + return + } + } + // We need to cache the schema as part of the definition because it is // used in the server to detect relationships. It would be far better to // instead fetch the schema in the backend at runtime. + // If _tableId is explicitly included in the update request, the schema will be requested let schema - if (e.detail?.tableId) { - schema = getSchemaForDatasourcePlus(e.detail.tableId, { + if (request?._tableId) { + schema = getSchemaForDatasourcePlus(request._tableId, { searchableSchema: true, }).schema + delete request._tableId } try { if (isTestModal) { @@ -136,21 +487,22 @@ newTestData = { ...newTestData, body: { - [key]: e.detail, + ...update, ...$selectedAutomation.testData?.body, }, } } newTestData = { ...newTestData, - [key]: e.detail, + ...request, } await automationStore.actions.addTestDataToAutomation(newTestData) } else { - const data = { schema, [key]: e.detail } + const data = { schema, ...request } await automationStore.actions.updateBlockInputs(block, data) } } catch (error) { + console.error("Error saving automation", error) notifications.error("Error saving automation") } }) @@ -195,14 +547,17 @@ let runtimeName /* Begin special cases for generating custom schemas based on triggers */ - if (idx === 0 && automation.trigger?.event === "app:trigger") { + if ( + idx === 0 && + automation.trigger?.event === AutomationEventType.APP_TRIGGER + ) { return `trigger.fields.${name}` } if ( idx === 0 && - (automation.trigger?.event === "row:update" || - automation.trigger?.event === "row:save") + (automation.trigger?.event === AutomationEventType.ROW_UPDATE || + automation.trigger?.event === AutomationEventType.ROW_SAVE) ) { let noRowKeywordBindings = ["id", "revision", "oldRow"] if (!noRowKeywordBindings.includes(name)) return `trigger.row.${name}` @@ -277,7 +632,10 @@ } } - if (idx === 0 && automation.trigger?.event === "app:trigger") { + if ( + idx === 0 && + automation.trigger?.event === AutomationEventType.APP_TRIGGER + ) { schema = Object.fromEntries( Object.keys(automation.trigger.inputs.fields || []).map(key => [ key, @@ -286,8 +644,9 @@ ) } if ( - (idx === 0 && automation.trigger.event === "row:update") || - (idx === 0 && automation.trigger.event === "row:save") + (idx === 0 && + automation.trigger.event === AutomationEventType.ROW_UPDATE) || + (idx === 0 && automation.trigger.event === AutomationEventType.ROW_SAVE) ) { let table = $tables.list.find( table => table._id === automation.trigger.inputs.tableId @@ -353,10 +712,12 @@ function saveFilters(key) { const filters = QueryUtils.buildQuery(tempFilters) - const defKey = `${key}-def` - onChange({ detail: filters }, key) - // need to store the builder definition in the automation - onChange({ detail: tempFilters }, defKey) + + onChange({ + [key]: filters, + [`${key}-def`]: tempFilters, // need to store the builder definition in the automation + }) + drawer.hide() } @@ -373,6 +734,7 @@ value.customType !== "cron" && value.customType !== "triggerSchema" && value.customType !== "automationFields" && + value.customType !== "fields" && value.type !== "signature_single" && value.type !== "attachment" && value.type !== "attachment_single" @@ -381,11 +743,10 @@ function getFieldLabel(key, value) { const requiredSuffix = requiredProperties.includes(key) ? "*" : "" - return `${value.title || (key === "row" ? "Row" : key)} ${requiredSuffix}` - } - - function toggleTestDataRowVisibility(key) { - testDataRowVisibility[key] = !testDataRowVisibility[key] + const label = `${ + value.title || (key === "row" ? "Row" : key) + } ${requiredSuffix}` + return Helpers.capitalise(label) } function handleAttachmentParams(keyValueObj) { @@ -398,16 +759,6 @@ return params } - function toggleAttachmentBinding(e, key) { - onChange( - { - detail: "", - }, - key - ) - onChange({ detail: { useAttachmentBinding: e.detail } }, "meta") - } - onMount(async () => { try { await environment.loadVariables() @@ -417,122 +768,152 @@ }) -
- {#each schemaProperties as [key, value]} - {#if canShowField(key, value)} - {@const label = getFieldLabel(key, value)} -
- {#if key !== "fields" && value.type !== "boolean" && shouldRenderField(value)} - - {/if} -
- {#if value.type === "string" && value.enum && canShowField(key, value)} - onChange({ [key]: e.detail })} value={inputData[key]} - on:change={e => onChange(e, key)} + placeholder={false} + options={value.enum} + getOptionLabel={(x, idx) => + value.pretty ? value.pretty[idx] : x} /> - - {:else if value.customType === "column"} - onChange({ [key]: e.detail })} + value={inputData[key]} + options={Object.keys(table?.schema || {})} + /> + {:else if value.type === "attachment" || value.type === "signature_single"} +
+
+ +
+
+ { + onChange({ + [key]: null, + meta: { + useAttachmentBinding: e.detail, + }, + }) + }} + /> +
-
- {#if !inputData?.meta?.useAttachmentBinding} - - onChange( - { - detail: e.detail.map(({ name, value }) => ({ +
+ {#if !inputData?.meta?.useAttachmentBinding} + + onChange({ + [key]: e.detail.map(({ name, value }) => ({ url: name, filename: value, })), - }, - key - )} - object={handleAttachmentParams(inputData[key])} - allowJS - {bindings} - keyBindings - customButtonText={"Add attachment"} - keyPlaceholder={"URL"} - valuePlaceholder={"Filename"} - /> - {:else if isTestModal} - onChange(e, key)} - {bindings} - updateOnChange={false} - /> - {:else} -
+ })} + object={handleAttachmentParams(inputData[key])} + allowJS + {bindings} + keyBindings + customButtonText={value.type === "attachment" + ? "Add attachment" + : "Add signature"} + keyPlaceholder={"URL"} + valuePlaceholder={"Filename"} + /> + {:else if isTestModal} + onChange({ [key]: e.detail })} + {bindings} + updateOnChange={false} + /> + {:else} onChange(e, key)} + on:change={e => onChange({ [key]: e.detail })} {bindings} updateOnChange={false} placeholder={value.customType === "queryLimit" @@ -540,235 +921,158 @@ : ""} drawerLeft="260px" /> -
- {/if} + {/if} +
-
- {:else if value.customType === "filters"} - Define filters - - - - (tempFilters = e.detail)} - /> - - - {:else if value.customType === "password"} - onChange(e, key)} - value={inputData[key]} - /> - {:else if value.customType === "email"} - {#if isTestModal} - Define filters + + + + (tempFilters = e.detail)} + /> + + + {:else if value.customType === "cron"} + onChange({ [key]: e.detail })} value={inputData[key]} - panel={AutomationBindingPanel} - type="email" - on:change={e => onChange(e, key)} - {bindings} - updateOnChange={false} /> - {:else} - onChange({ [key]: e.detail })} value={inputData[key]} - on:change={e => onChange(e, key)} {bindings} - allowJS={false} - updateOnChange={false} - drawerLeft="260px" /> - {/if} - {:else if value.customType === "query"} - onChange(e, key)} - value={inputData[key]} - /> - {:else if value.customType === "cron"} - onChange(e, key)} - value={inputData[key]} - /> - {:else if value.customType === "automationFields"} - onChange(e, key)} - value={inputData[key]} - {bindings} - /> - {:else if value.customType === "queryParams"} - onChange(e, key)} - value={inputData[key]} - {bindings} - /> - {:else if value.customType === "table"} - onChange(e, key)} - /> - {:else if value.customType === "row"} - {#if isTestModal} -
- toggleTestDataRowVisibility(key)} - /> - -
- {#if testDataRowVisibility[key]} - { - if (e.detail?.key) { - onChange(e, e.detail.key) - } else { - onChange(e, key) - } - }} - {bindings} - {isTestModal} - {isUpdateRow} - /> - {/if} - - {:else} - onChange({ [key]: e.detail })} value={inputData[key]} - meta={inputData["meta"] || {}} - on:change={e => { - if (e.detail?.key) { - onChange(e, e.detail.key) - } else { - onChange(e, key) - } - }} + {bindings} + /> + {:else if value.customType === "table"} + onChange({ [key]: e.detail })} + /> + {:else if value.customType === "webhookUrl"} + + {:else if value.customType === "fields"} + onChange({ [key]: e.detail })} {bindings} {isTestModal} - {isUpdateRow} /> - {/if} - {:else if value.customType === "webhookUrl"} - onChange(e, key)} - value={inputData[key]} - /> - {:else if value.customType === "fields"} - onChange(e, key)} - {bindings} - {isTestModal} - /> - {:else if value.customType === "triggerSchema"} - onChange(e, key)} - value={inputData[key]} - /> - {:else if value.customType === "code"} - -
-
- { - // need to pass without the value inside - onChange({ detail: e.detail }, key) - inputData[key] = e.detail - }} - completions={stepCompletions} - mode={codeMode} - autocompleteEnabled={codeMode !== EditorModes.JS} - bind:getCaretPosition - bind:insertAtPos - placeholder={codeMode === EditorModes.Handlebars - ? "Add bindings by typing {{" - : null} - /> -
- {#if editingJs} -
- - bindingsHelpers.onSelectBinding( - inputData[key], - binding, - { - js: true, - dontDecode: true, - type: BindingType.RUNTIME, - } - )} - mode="javascript" + {:else if value.customType === "triggerSchema"} + onChange({ [key]: e.detail })} + value={inputData[key]} + /> + {:else if value.customType === "code"} + +
+
+ { + // need to pass without the value inside + onChange({ [key]: e.detail }) + inputData[key] = e.detail + }} + completions={stepCompletions} + mode={codeMode} + autocompleteEnabled={codeMode !== EditorModes.JS} + bind:getCaretPosition + bind:insertAtPos + placeholder={codeMode === EditorModes.Handlebars + ? "Add bindings by typing {{" + : null} />
- {/if} -
-
- {:else if value.customType === "loopOption"} - onChange({ [key]: e.detail })} + autoWidth value={inputData[key]} - panel={AutomationBindingPanel} - type={value.customType} - on:change={e => onChange(e, key)} - {bindings} - updateOnChange={false} + options={["Array", "String"]} + defaultValue={"Array"} /> - {:else} -
- onChange(e, key)} + on:change={e => onChange({ [key]: e.detail })} {bindings} updateOnChange={false} - placeholder={value.customType === "queryLimit" - ? queryLimit - : ""} - drawerLeft="260px" /> -
+ {:else} +
+ onChange({ [key]: e.detail })} + {bindings} + updateOnChange={false} + placeholder={value.customType === "queryLimit" + ? queryLimit + : ""} + drawerLeft="260px" + /> +
+ {/if} {/if} - {/if} +
-
- {/if} - {/each} + {/if} + {/each} + {/if}
+ -{#if stepId === TriggerStepID.WEBHOOK} +{#if stepId === TriggerStepID.WEBHOOK && !isTestModal} {/if} @@ -777,18 +1081,12 @@ width: 320px; } - .align-horizontally { - display: flex; - gap: var(--spacing-s); - align-items: center; - } - - .fields { + .step-fields { display: flex; flex-direction: column; justify-content: flex-start; align-items: stretch; - gap: var(--spacing-s); + gap: var(--spacing-l); } .block-field { @@ -808,10 +1106,6 @@ margin-top: var(--spacing-s); } - .test :global(.drawer) { - width: 10000px !important; - } - .js-editor { display: flex; flex-direction: row; diff --git a/packages/builder/src/components/automation/SetupPanel/FieldSelector.svelte b/packages/builder/src/components/automation/SetupPanel/FieldSelector.svelte index 3920885a2e..7dd38ee44e 100644 --- a/packages/builder/src/components/automation/SetupPanel/FieldSelector.svelte +++ b/packages/builder/src/components/automation/SetupPanel/FieldSelector.svelte @@ -1,19 +1,28 @@ -{#if schemaFields.length && isTestModal} -
+{#if schemaFields?.length && isTestModal} +
{#each schemaFields as [field, schema]} - + + {#if [STRING, NUMBER, ARRAY].includes(schema.type)} + onChange(e, field)} + type="string" + {bindings} + allowJS={true} + updateOnChange={false} + title={schema.name} + autocomplete="off" + /> + {:else if schema.type === "boolean"} + table._id !== TableNames.USERS)} - getOptionLabel={table => table.name} - getOptionValue={table => table._id} - /> -
-
-{#if schemaFields.length} - {#each schemaFields as [field, schema]} - {#if !schema.autocolumn} -
- -
- {#if isTestModal} +{#each schemaFields || [] as [field, schema]} + {#if !schema.autocolumn && Object.hasOwn(editableFields, field)} + +
+ {#if isTestModal} + + {:else} + + onChange({ + row: { + [field]: e.detail, + }, + })} + {bindings} + allowJS={true} + updateOnChange={false} + drawerLeft="260px" + > onChange(change)} /> - {:else} - onChange(e, field)} - {bindings} - allowJS={true} - updateOnChange={false} - drawerLeft="260px" - > - - - {/if} - - {#if isUpdateRow && schema.type === "link"} -
- - onChangeSetting(field, "clearRelationships", e.detail)} - /> -
- {/if} -
+ + {/if}
- {/if} - {/each} + + {/if} +{/each} + +{#if table && schemaFields} + {#key editableFields} +
+ { + customPopover.show() + }} + disabled={!schemaFields} + >Add fields + +
+ {/key} {/if} + + + +
    + {#each schemaFields || [] as [field, schema]} + {#if !schema.autocolumn} +
  • { + if (Object.hasOwn(editableFields, field)) { + delete editableFields[field] + onChange({ + meta: { fields: editableFields }, + row: { [field]: null }, + }) + } else { + editableFields[field] = {} + onChange({ meta: { fields: editableFields } }) + } + }} + > + +
    {field}
    + +
  • + {/if} + {/each} +
+
+ diff --git a/packages/builder/src/components/automation/SetupPanel/RowSelectorTypes.svelte b/packages/builder/src/components/automation/SetupPanel/RowSelectorTypes.svelte index a43ff35c80..85d57e665a 100644 --- a/packages/builder/src/components/automation/SetupPanel/RowSelectorTypes.svelte +++ b/packages/builder/src/components/automation/SetupPanel/RowSelectorTypes.svelte @@ -11,17 +11,18 @@ import DrawerBindableInput from "../../common/bindings/DrawerBindableInput.svelte" import ModalBindableInput from "../../common/bindings/ModalBindableInput.svelte" import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte" - import Editor from "components/integration/QueryEditor.svelte" + import CodeEditor from "components/common/CodeEditor/CodeEditor.svelte" import KeyValueBuilder from "components/integration/KeyValueBuilder.svelte" export let onChange export let field export let schema export let value + export let meta export let bindings export let isTestModal - export let useAttachmentBinding - export let onChangeSetting + + $: fieldData = value[field] $: parsedBindings = bindings.map(binding => { let clone = Object.assign({}, binding) @@ -35,14 +36,15 @@ FieldType.SIGNATURE_SINGLE, ] - let previousBindingState = useAttachmentBinding - function schemaHasOptions(schema) { return !!schema.constraints?.inclusion?.length } function handleAttachmentParams(keyValueObj) { let params = {} + if (!keyValueObj) { + return null + } if (!Array.isArray(keyValueObj) && keyValueObj) { keyValueObj = [keyValueObj] @@ -50,45 +52,68 @@ if (keyValueObj.length) { for (let param of keyValueObj) { - params[param.url] = param.filename + params[param.url || ""] = param.filename || "" } } return params } - async function handleToggleChange(toggleField, event) { - if (event.detail === true) { - value[toggleField] = [] - } else { - value[toggleField] = "" - } - previousBindingState = event.detail - onChangeSetting(toggleField, "useAttachmentBinding", event.detail) - onChange({ detail: value[toggleField] }, toggleField) - } + const handleMediaUpdate = e => { + const media = e.detail || [] + const isSingle = + schema.type === FieldType.ATTACHMENT_SINGLE || + schema.type === FieldType.SIGNATURE_SINGLE + const parsedMedia = media.map(({ name, value }) => ({ + url: name, + filename: value, + })) - $: if (useAttachmentBinding !== previousBindingState) { - if (useAttachmentBinding) { - value[field] = [] - } else { - value[field] = "" + if (isSingle) { + const [singleMedia] = parsedMedia + // Return only the first entry + return singleMedia + ? { + url: singleMedia.url, + filename: singleMedia.filename, + } + : null } - previousBindingState = useAttachmentBinding + + // Return the entire array + return parsedMedia } {#if schemaHasOptions(schema) && schema.type !== "array"} onChange(e, field)} - value={value[field]} + on:change={e => + onChange({ + row: { + [field]: e.detail, + }, + })} + value={fieldData} options={[ { label: "True", value: "true" }, { label: "False", value: "false" }, @@ -96,83 +121,111 @@ /> {:else if schemaHasOptions(schema) && schema.type === "array"} onChange(e, field)} + on:change={e => + onChange({ + row: { + [field]: e.detail, + }, + })} /> {:else if schema.type === "longform"} -