diff --git a/docs/docs/index.mdx b/docs/docs/index.mdx index bb204678b5df..f998162c97d0 100644 --- a/docs/docs/index.mdx +++ b/docs/docs/index.mdx @@ -58,7 +58,7 @@ A modern, enterprise-ready business intelligence web application. [**Why Superset?**](#why-superset) | [**Supported Databases**](#supported-databases) | [**Installation and Configuration**](#installation-and-configuration) | -[**Release Notes**](https://github.com/apache/superset/blob/master/RELEASING/README.md#release-notes-for-recent-releases) | +[**Release Notes**](https://github.com/apache/superset/releases) | [**Get Involved**](#get-involved) | [**Contributor Guide**](#contributor-guide) | [**Resources**](#resources) | diff --git a/docs/package.json b/docs/package.json index ec68d44e32ed..37a981ae03a3 100644 --- a/docs/package.json +++ b/docs/package.json @@ -66,7 +66,7 @@ "caniuse-lite": "^1.0.30001809", "docusaurus-plugin-openapi-docs": "^5.2.0", "docusaurus-theme-openapi-docs": "^5.2.0", - "js-yaml": "^5.3.0", + "js-yaml": "^5.4.0", "json-bigint": "^1.0.0", "prism-react-renderer": "^2.4.1", "react": "^18.3.1", @@ -87,7 +87,7 @@ "@docusaurus/module-type-aliases": "^3.10.2", "@types/js-yaml": "^4.0.9", "@types/react": "^19.1.8", - "oxfmt": "^0.64.0", + "oxfmt": "^0.65.0", "oxlint": "^1.80.0", "oxlint-tsgolint": "^7.0.2001", "typescript": "7.0.2", diff --git a/docs/yarn.lock b/docs/yarn.lock index 89deec370336..d0f23d63f6cf 100644 --- a/docs/yarn.lock +++ b/docs/yarn.lock @@ -3078,100 +3078,100 @@ resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-win32-x64-msvc/-/binding-win32-x64-msvc-11.21.2.tgz#dbdf12721396ef4c31899d0963b002df32f57050" integrity sha512-VPoCAhKvCQTlG7vxqaBXcmuvbh77BfnGXj8g0pbvVXpm1F/R8rDVwqIWcEbMzrI1JvJlm8v7T9uMVQb6UctMRg== -"@oxfmt/binding-android-arm-eabi@0.64.0": - version "0.64.0" - resolved "https://registry.yarnpkg.com/@oxfmt/binding-android-arm-eabi/-/binding-android-arm-eabi-0.64.0.tgz#e14e25c032f6d8a6b025eb5ee7bb606c3cbdd10e" - integrity sha512-o6uzh/jTOQeAY5TdkAeXdqv7MBRcPxiRA08zrcBtkKj5cSu/FMu0Hl7Q6Fi1KCKyCWZ6lJVjBzdsJvsKltUsGQ== - -"@oxfmt/binding-android-arm64@0.64.0": - version "0.64.0" - resolved "https://registry.yarnpkg.com/@oxfmt/binding-android-arm64/-/binding-android-arm64-0.64.0.tgz#294a15b8402eedde0e0a467748e3efadf61bf523" - integrity sha512-jRGSUeeP7p3Gynw2YaCVtjBIA6ZxY6bEB/ES5i54OhqmRTyuVg7ZgstEtzgq6GOAJd+2QZ5pvf+bFfmW5Mp9cw== - -"@oxfmt/binding-darwin-arm64@0.64.0": - version "0.64.0" - resolved "https://registry.yarnpkg.com/@oxfmt/binding-darwin-arm64/-/binding-darwin-arm64-0.64.0.tgz#d55b1a5d5d97d4ccde8e4be7b63e06e4e56f2d13" - integrity sha512-JINwtU2lW7nOFSqi+H2qplipNUqah9Gc1jgGmB82kTD4UnZrZIVxCJ9qEmFiKfjNq27gYLFhrUb0to86aCwMjw== - -"@oxfmt/binding-darwin-x64@0.64.0": - version "0.64.0" - resolved "https://registry.yarnpkg.com/@oxfmt/binding-darwin-x64/-/binding-darwin-x64-0.64.0.tgz#1c9673270ed597ba9456d40fa0607d50e81158ea" - integrity sha512-gCmuswrgrOSajV4HCRFkVCGIruPq8bjYuPYgSE2WQB3mD6XrdyZ3JMSRZCkQ8zCxOyGWriBo6QoZ5nmMHQ1BfA== - -"@oxfmt/binding-freebsd-x64@0.64.0": - version "0.64.0" - resolved "https://registry.yarnpkg.com/@oxfmt/binding-freebsd-x64/-/binding-freebsd-x64-0.64.0.tgz#9e8f8b3a5a558043c664d43d54e441756af30c56" - integrity sha512-Ab8g7a38pT0MMImjh7anRSTve6buWBIlcXIFBYa5xl4s6UxEgKSc2xOOhbGtLwvXnEi2PsEDGoJh3oUU7xkehQ== - -"@oxfmt/binding-linux-arm-gnueabihf@0.64.0": - version "0.64.0" - resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.64.0.tgz#cfe552538c9e9402ca64d7b83b1ccf02457ef391" - integrity sha512-BgvS3CoQ+Xy2deoZqEN8JVKabcCZi2RxA3yant8G9OAv9KuPJ9TCjHkqigzdHUVwErZxEP5d2bzLIEyKYyBDLg== - -"@oxfmt/binding-linux-arm-musleabihf@0.64.0": - version "0.64.0" - resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.64.0.tgz#1944e367da59e8b1770c5ba96465d0c7e640053e" - integrity sha512-QXpNxwoMj0YvnceCNZadNSden3bIcnvjn/sDp/rwZhRoZoZYGpHvtPyhGsdJz9uvT9GkaMW7SsLddurU56dt8w== - -"@oxfmt/binding-linux-arm64-gnu@0.64.0": - version "0.64.0" - resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.64.0.tgz#510386113bf6a128cf3106d612471dbd1a13b0f4" - integrity sha512-BBgH3I1ppDsI5pZ4Pdhw0ceYxwVCfbU/bZEBCeZ6caRS9x0ZabErxubP7riGUn11PXZBhe8DYdjkDKP1FlVQ5w== - -"@oxfmt/binding-linux-arm64-musl@0.64.0": - version "0.64.0" - resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.64.0.tgz#7235405901cb0368b659eb42b362a817fc3330a3" - integrity sha512-v19HSjC/BGXdt26qEvKZtwAHgGmQ2Agcap2kQP+KIqoRZqivVzYth3ui2dJA1i+6/fjpjga85lIOaJJjQ/bOOw== - -"@oxfmt/binding-linux-ppc64-gnu@0.64.0": - version "0.64.0" - resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.64.0.tgz#1f0563c530dfa634682ffa32d16830404b95a8c6" - integrity sha512-PElLnOo4xFTBZrxPhgTIj0eHqZXwEBQoNWtb7facUV170T0B0FRET0iNbb3LUeLWTybkUW+vsdyv4ihOdyXGyw== - -"@oxfmt/binding-linux-riscv64-gnu@0.64.0": - version "0.64.0" - resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.64.0.tgz#36f55e955c5b38b587470f181146c9a11cf8bdb1" - integrity sha512-Qzsg15n4F5CH+MorcRW4MkAEMiLzXmeG+DiDSbP/bBTqCmWOH3K9DHryNrve+JHlV0txS+B6Z9P5Xz+cmWeL+g== - -"@oxfmt/binding-linux-riscv64-musl@0.64.0": - version "0.64.0" - resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.64.0.tgz#bb9c6c3860c8832fe271623eb6131ea5f5e094cd" - integrity sha512-/GZ358wnQ/Ez4UVnCcZIi56JkY0sOdZ+B108pqXKqZz3jLS59F4KEAB1Qv3fRlObrFEk+3L2vUQ/xoPx+3vjXw== - -"@oxfmt/binding-linux-s390x-gnu@0.64.0": - version "0.64.0" - resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.64.0.tgz#7d736d923f3c7f88743f26479a49903c6dbaf818" - integrity sha512-/C9We3DXegowfLXtVCYHeNiU9azwCDr5cQkEtCVlc74vyn+lLQSPApJ1CZmxAduqeq/Oi3gQ+IVptyhCaTMtkQ== - -"@oxfmt/binding-linux-x64-gnu@0.64.0": - version "0.64.0" - resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.64.0.tgz#34dfe2bde9ed124324b45aae078618456e850452" - integrity sha512-91KM2CeRWscIEHlj1NsW2WSnzGeq1Ehq+39bfDowTdkn+fcvK/x4Y1RcyqT7glyBjZio0ldkeCG6Usj3v7ASog== - -"@oxfmt/binding-linux-x64-musl@0.64.0": - version "0.64.0" - resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-x64-musl/-/binding-linux-x64-musl-0.64.0.tgz#b5edc644409aff9715279650767d34d2fb65d59a" - integrity sha512-gw7uEk9I+7zoT1EYLra1eWArIzNcz8e3jkv+Noo2+o2T7wPvsNSQbfoa4DSfZlvn1i6mJ05RiZ4/omaXPDNhQg== - -"@oxfmt/binding-openharmony-arm64@0.64.0": - version "0.64.0" - resolved "https://registry.yarnpkg.com/@oxfmt/binding-openharmony-arm64/-/binding-openharmony-arm64-0.64.0.tgz#6b1d9c662e08bf5fbc1e9ccdb45ed28c004b90c4" - integrity sha512-HYHFf616FHSPSO07c09mjmXBfQ73wIVM3m0txOiooa5XZkGoxFd6B14PVj0LB0DXIqJ6wAO/dDR/NX/5UUaqnw== - -"@oxfmt/binding-win32-arm64-msvc@0.64.0": - version "0.64.0" - resolved "https://registry.yarnpkg.com/@oxfmt/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.64.0.tgz#bc5a005e159a8f9af4168eed2e61fe477f4029db" - integrity sha512-uQjFp081IZSWD6VAofX2iO2z01awAdHmfC+NrieWIPKrT2hZKQDyq/U18M7ifC0sm0Wz8aHY/p6+FDYIzs/CrQ== - -"@oxfmt/binding-win32-ia32-msvc@0.64.0": - version "0.64.0" - resolved "https://registry.yarnpkg.com/@oxfmt/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.64.0.tgz#88e90b96f7b39e4b6f75178c94c52d464fa58b53" - integrity sha512-lNM6byTAQ881jugzFu8juJTbNRgsUTlswMA6pJmwi1XDvmIqnnb49lcUAs5gz94fCJLrVN+/X3s3jOKqx23WIQ== - -"@oxfmt/binding-win32-x64-msvc@0.64.0": - version "0.64.0" - resolved "https://registry.yarnpkg.com/@oxfmt/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.64.0.tgz#788c7fe26f89e57269f79e8f8a34e9b1497bc674" - integrity sha512-BtmbtL/QjMtF1a6C3CqoDluH2IfB6fJt62E+B9RFfUPtFk4Iz9PFS6+y/SzzOvSxc7aUk2Kphwg7Dh8lMbwu6g== +"@oxfmt/binding-android-arm-eabi@0.65.0": + version "0.65.0" + resolved "https://registry.yarnpkg.com/@oxfmt/binding-android-arm-eabi/-/binding-android-arm-eabi-0.65.0.tgz#1d9be45cec55c86b17e1feffbffaeadd45fd2c9a" + integrity sha512-M10Gs1SSpTNI6ahGx3M/OlIdUF4hkaP6OgUb+MS79t/Pgflk3r1nW5gPFqsZGUAXg0H1AfANT9AvLdBSTIhZKg== + +"@oxfmt/binding-android-arm64@0.65.0": + version "0.65.0" + resolved "https://registry.yarnpkg.com/@oxfmt/binding-android-arm64/-/binding-android-arm64-0.65.0.tgz#7a6dbd301c1198d2a16439e005ca33fb3ebd2f62" + integrity sha512-6DXH5sftNlaHpWJG50hFMF+Qxtq5D2TmahvcDPxWNcGIf8qrC9Y0YgHYcYZ2hlWzaccKXh/f3GcssH8vtkl4JA== + +"@oxfmt/binding-darwin-arm64@0.65.0": + version "0.65.0" + resolved "https://registry.yarnpkg.com/@oxfmt/binding-darwin-arm64/-/binding-darwin-arm64-0.65.0.tgz#7fa69acf25aefb7ff832aaa207ece9f55c48398e" + integrity sha512-K9m7lr53pcOLETNsC88sWes/GWHUGjZyHx95UhYcSXy0r30haLdeXlSufSenEAtoLaW753WN8/l4M7GYcRt6cg== + +"@oxfmt/binding-darwin-x64@0.65.0": + version "0.65.0" + resolved "https://registry.yarnpkg.com/@oxfmt/binding-darwin-x64/-/binding-darwin-x64-0.65.0.tgz#c315ca1fb9fb606477dc76fc4154d92c32a244a9" + integrity sha512-sTNwIx1gre3MyiHOPLu7IGW4UyMScYL4DTmJT01p4vzB0En+OJUQz6KuH8t0PpsClRSaMuY3b0QmtoPItfO8Lg== + +"@oxfmt/binding-freebsd-x64@0.65.0": + version "0.65.0" + resolved "https://registry.yarnpkg.com/@oxfmt/binding-freebsd-x64/-/binding-freebsd-x64-0.65.0.tgz#4815e6ac50a9f38cd93dda3d649c6988bded481d" + integrity sha512-lYZMVIiIpnjGu5hJb2jxA8NYQ/e0OTGuaiAf4dqlGPNnPmUTu23FZRMltmjro/KkQm1uE4NT4n5yJ2zWmKcpfA== + +"@oxfmt/binding-linux-arm-gnueabihf@0.65.0": + version "0.65.0" + resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.65.0.tgz#36c1c82b18186293c62c48fbcf1e3e4bdc9cd72d" + integrity sha512-gIdXFAt/bURnjxuoedDEWdZ0PEWEmdDcm8qdpoFYYvW3QMk/5D4vUaH4mlMeRpeTdST4izUgHVO6RawQ4QulJw== + +"@oxfmt/binding-linux-arm-musleabihf@0.65.0": + version "0.65.0" + resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.65.0.tgz#623880e16cd16fb36abf02aa050e5e33d1acbf67" + integrity sha512-jJVyADto7gA2AaX5qAjAexrxx9PJQaKWOe8PICE7yKMbjBRyOHcmj9TtVJ+MZYDUQ3hodU0AcoTj0jFQ1W4C6Q== + +"@oxfmt/binding-linux-arm64-gnu@0.65.0": + version "0.65.0" + resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.65.0.tgz#1570f61b877385b9c1b56800663f68e8f988c533" + integrity sha512-p3RFkB+u7u+8up99b/NEcI1hdpLDiGgJYNwDorB60n7eH+eKposAKuMBxx+NqB3b+sJP4CZmYDh9G7X62tUsKg== + +"@oxfmt/binding-linux-arm64-musl@0.65.0": + version "0.65.0" + resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.65.0.tgz#e7494ed26c0cd52a1ac4b38fe5f0a2a88231cd2e" + integrity sha512-5Prb0uFzJHr+OUD/qS/TmU526wD+PaHDsm3KoRiUXbMIDpTSErjeQYkK3OQeshAvD/PuLa9WGEi9WPajjdOZJg== + +"@oxfmt/binding-linux-ppc64-gnu@0.65.0": + version "0.65.0" + resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.65.0.tgz#e87bc27e8d9fba5eaba59b11a1f00ad1e116f803" + integrity sha512-S8svxTp81obnF3admN9yd+u2rOYXtyzThLGBTg1PY6TPtGcC09BaaXLQD+TBSMa7yvqhCDZ8DFri+S/yG60qCg== + +"@oxfmt/binding-linux-riscv64-gnu@0.65.0": + version "0.65.0" + resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.65.0.tgz#dd27e2b20b27817938815a2b47f87fcf41ca1aa0" + integrity sha512-WtXBr75G/h2qOHy8SiGtC1R6aS3jt4mE52v1D8AtwMXIgoOmSNP9lKvbSaTRoL0e5wsMPoi6T72QWDYPu+S+nA== + +"@oxfmt/binding-linux-riscv64-musl@0.65.0": + version "0.65.0" + resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.65.0.tgz#dc37b95c1f07579eeedcf869d7cf3b47ca695ac9" + integrity sha512-YwSLVvpaz4o/nv/miiPEBJz+eJ+VmbgNIrao6RccK9ce+L5EA8wP+ZD0uFeq6wKOza6zoWv/dR0sj6lip6R3EA== + +"@oxfmt/binding-linux-s390x-gnu@0.65.0": + version "0.65.0" + resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.65.0.tgz#c898674718ce8da7a7c625f3b3bcda0b0ff55bc1" + integrity sha512-XQTPqgvyrgkKcFq+Tp2eK6JS7sqqJ+nRmy2Fav4j3I+i4dJoPJm7YwEdoeSDX9xkqj9jZ/lWfF3bXUWztIrn6A== + +"@oxfmt/binding-linux-x64-gnu@0.65.0": + version "0.65.0" + resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.65.0.tgz#720a641158804551480d79cdc85cfed21b0882c8" + integrity sha512-cjZlx6S/VkeCNWCbwZriTnLnZeTcV3DEyeRGSw/2wwLP9viq+C0bJ4bC1k/ZLkFxDcB1lUgSasPkYGP1bdraOg== + +"@oxfmt/binding-linux-x64-musl@0.65.0": + version "0.65.0" + resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-x64-musl/-/binding-linux-x64-musl-0.65.0.tgz#6cb3e1f25c85c619ce634b2f7e3238c33caafe86" + integrity sha512-2azCjxdLtK4zCcIOU1dlXlU0xxfbPi6EjwWx7Ac7teWPidIIDOcIhudup83xNCKYhtqeVd/gaVDOxbUq4syXWA== + +"@oxfmt/binding-openharmony-arm64@0.65.0": + version "0.65.0" + resolved "https://registry.yarnpkg.com/@oxfmt/binding-openharmony-arm64/-/binding-openharmony-arm64-0.65.0.tgz#c620f9f383c544428f363bf4418b94b81b5b9346" + integrity sha512-KXQ7xi1e/voP0IQaw6fG6XY4Z5+Llf1XmRSZS1t7pVFCecFJ0iXaboKmVwjFtp5MLlT5iWQrJ2U1C3GJdZ2u+Q== + +"@oxfmt/binding-win32-arm64-msvc@0.65.0": + version "0.65.0" + resolved "https://registry.yarnpkg.com/@oxfmt/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.65.0.tgz#79c59706cc5450854d57d582116b13d6611e875c" + integrity sha512-2FbbjG5jEqLSLKVJwBap84uJfpn5Y5A53KEO0aUNr+zeiRB9nyPUIFMcSbZVMFLitfBytFWRNngozXYjb6Rsbw== + +"@oxfmt/binding-win32-ia32-msvc@0.65.0": + version "0.65.0" + resolved "https://registry.yarnpkg.com/@oxfmt/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.65.0.tgz#6a52067c7fc666ee46a1b95366c08a9a1c9b26a1" + integrity sha512-LJ+ZacAPSjegDOnSLyA1TMWAhdDrsK4el3REdr1oL2UtVBCMhO2II/Sb3cEW6mF2MfLhl8hDNCSvc7KSbgk3LQ== + +"@oxfmt/binding-win32-x64-msvc@0.65.0": + version "0.65.0" + resolved "https://registry.yarnpkg.com/@oxfmt/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.65.0.tgz#f63624f39bbfddcd679d471c15879176581bb32b" + integrity sha512-higu9cWEO6XXFzATD1jf0mCK34rNfN2H9JrJie7QB1IhleVpTh0QlLH9Ip2C1H/Nd5n0v5pvRtC+5R0uE4HpVg== "@oxlint-tsgolint/darwin-arm64@7.0.2001": version "7.0.2001" @@ -6471,7 +6471,7 @@ base64-js@^1.3.1, base64-js@^1.5.1: resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== -baseline-browser-mapping@^2.10.38, baseline-browser-mapping@^2.11.18, baseline-browser-mapping@^2.9.19: +baseline-browser-mapping@^2.11.12, baseline-browser-mapping@^2.11.18, baseline-browser-mapping@^2.9.19: version "2.11.18" resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.11.18.tgz#49701f6ab6c58ccafb6d8e1ab2767574d9f0ba73" integrity sha512-1iEmLEYSiE1SeBoAfPo/Mnx3PzfzHUkDK61ASkCpuk3YXugYLH5DYK1SzqV55F8FMI6s0F+/tCP7Polz1QRjxw== @@ -6578,15 +6578,15 @@ braces@^3.0.3, braces@~3.0.2: fill-range "^7.1.1" browserslist@^4.0.0, browserslist@^4.23.0, browserslist@^4.24.0, browserslist@^4.24.2, browserslist@^4.28.1, browserslist@^4.28.4: - version "4.28.4" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.28.4.tgz#dd8b8167a32845ff5f8cd6ce13f5abba16cd04c9" - integrity sha512-MTc8i/x9jBQd1iMw2CFGS+rwMa07eYjLR0CCTLDACl9xhxy+nIs3KeML/biicXtk9JrZ6dnnTatmc7ErPXIxqw== + version "4.28.8" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.28.8.tgz#a3c79ceb70028527e5da7dafc887f3200b5168c0" + integrity sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA== dependencies: - baseline-browser-mapping "^2.10.38" - caniuse-lite "^1.0.30001799" - electron-to-chromium "^1.5.376" - node-releases "^2.0.48" - update-browserslist-db "^1.2.3" + baseline-browser-mapping "^2.11.12" + caniuse-lite "^1.0.30001809" + electron-to-chromium "^1.5.402" + node-releases "^2.0.53" + update-browserslist-db "^1.3.0" buffer-from@^1.0.0: version "1.1.2" @@ -8095,10 +8095,10 @@ ee-first@1.1.1: resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== -electron-to-chromium@^1.5.376: - version "1.5.387" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.387.tgz#d3ce821b0a37af5a46e2d2a33379ecfa16303636" - integrity sha512-TaxwufTFDufvPEoXdhwVrA3UdFWBeWGkYoJ1K8ldF1xe6gKfth6iRNS5lTQ5JPNOHdGQm8PT1QYKUqFLCiUefQ== +electron-to-chromium@^1.5.402: + version "1.5.418" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.418.tgz#40ccbf6d572447663041611e05924b53bc9a0331" + integrity sha512-UzS26r3AEbG5wSoGVpJKqwHIU9zwQN7LHdVIThDrJpS0I5KdlXFMEb8543fhc9dVnIIAST6ar8rhwa00AL5MlA== emoji-regex@^8.0.0: version "8.0.0" @@ -9703,10 +9703,10 @@ js-yaml@4.1.0, js-yaml@=4.3.1, js-yaml@^4.1.0, js-yaml@^4.2.0, js-yaml@^4.3.0: dependencies: argparse "^2.0.1" -js-yaml@^5.3.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-5.3.0.tgz#526430a6da31065127528ae695ce168cfc5f91f0" - integrity sha512-muutsYr+e2+d3rTgUGslq5rxbBlUy3cJ61IsHag2QNDQV+7zXWjkUpmALIajhrlLlrgRUiymj6U3zUr/TMK84Q== +js-yaml@^5.4.0: + version "5.4.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-5.4.0.tgz#246dae8988029a6e9b19963e3ff5a1ff6e41f344" + integrity sha512-jE7vUJIebKzYQI5xu4co5CRBDlDEYnHrdzsxs4O2giCz4v2SbVMYKpmt1D9L38OKQAeCWmrOTRiCV93u0UkaJA== dependencies: argparse "^2.0.1" @@ -11288,10 +11288,10 @@ node-readfiles@^0.2.0: dependencies: es6-promise "^3.2.1" -node-releases@^2.0.48: - version "2.0.50" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.50.tgz#597197a852071ce42fc2550e58e223242bcba969" - integrity sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg== +node-releases@^2.0.53: + version "2.0.54" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.54.tgz#09af17d5647aa9f221ec5cf2becb95b68a981afe" + integrity sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ== normalize-path@^3.0.0, normalize-path@~3.0.0: version "3.0.0" @@ -11576,32 +11576,32 @@ oxc-resolver@11.21.2: "@oxc-resolver/binding-win32-arm64-msvc" "11.21.2" "@oxc-resolver/binding-win32-x64-msvc" "11.21.2" -oxfmt@^0.64.0: - version "0.64.0" - resolved "https://registry.yarnpkg.com/oxfmt/-/oxfmt-0.64.0.tgz#666a5148cdf7385007cd46e35e8ff8f94ecfd96b" - integrity sha512-XZ4GFBN/PLbXKq+0zrgpQfPKYuJlUuj+nzZJY7UpIbFMNyefNLCdN9EwViycNqnYcv0wrn0jXcQLlqJp8RCKBg== +oxfmt@^0.65.0: + version "0.65.0" + resolved "https://registry.yarnpkg.com/oxfmt/-/oxfmt-0.65.0.tgz#43f38114a6d9ad0952741c13c49f70ff1a4208d6" + integrity sha512-SgS5VgnP42T0zl3zWD+xoH8FCqg1SAFnSRoOT/qeoa6gxcYIqrDMOmcXIg/EWSN92Du4ogB4riuKhKd6Y4CGhw== dependencies: tinypool "2.1.0" optionalDependencies: - "@oxfmt/binding-android-arm-eabi" "0.64.0" - "@oxfmt/binding-android-arm64" "0.64.0" - "@oxfmt/binding-darwin-arm64" "0.64.0" - "@oxfmt/binding-darwin-x64" "0.64.0" - "@oxfmt/binding-freebsd-x64" "0.64.0" - "@oxfmt/binding-linux-arm-gnueabihf" "0.64.0" - "@oxfmt/binding-linux-arm-musleabihf" "0.64.0" - "@oxfmt/binding-linux-arm64-gnu" "0.64.0" - "@oxfmt/binding-linux-arm64-musl" "0.64.0" - "@oxfmt/binding-linux-ppc64-gnu" "0.64.0" - "@oxfmt/binding-linux-riscv64-gnu" "0.64.0" - "@oxfmt/binding-linux-riscv64-musl" "0.64.0" - "@oxfmt/binding-linux-s390x-gnu" "0.64.0" - "@oxfmt/binding-linux-x64-gnu" "0.64.0" - "@oxfmt/binding-linux-x64-musl" "0.64.0" - "@oxfmt/binding-openharmony-arm64" "0.64.0" - "@oxfmt/binding-win32-arm64-msvc" "0.64.0" - "@oxfmt/binding-win32-ia32-msvc" "0.64.0" - "@oxfmt/binding-win32-x64-msvc" "0.64.0" + "@oxfmt/binding-android-arm-eabi" "0.65.0" + "@oxfmt/binding-android-arm64" "0.65.0" + "@oxfmt/binding-darwin-arm64" "0.65.0" + "@oxfmt/binding-darwin-x64" "0.65.0" + "@oxfmt/binding-freebsd-x64" "0.65.0" + "@oxfmt/binding-linux-arm-gnueabihf" "0.65.0" + "@oxfmt/binding-linux-arm-musleabihf" "0.65.0" + "@oxfmt/binding-linux-arm64-gnu" "0.65.0" + "@oxfmt/binding-linux-arm64-musl" "0.65.0" + "@oxfmt/binding-linux-ppc64-gnu" "0.65.0" + "@oxfmt/binding-linux-riscv64-gnu" "0.65.0" + "@oxfmt/binding-linux-riscv64-musl" "0.65.0" + "@oxfmt/binding-linux-s390x-gnu" "0.65.0" + "@oxfmt/binding-linux-x64-gnu" "0.65.0" + "@oxfmt/binding-linux-x64-musl" "0.65.0" + "@oxfmt/binding-openharmony-arm64" "0.65.0" + "@oxfmt/binding-win32-arm64-msvc" "0.65.0" + "@oxfmt/binding-win32-ia32-msvc" "0.65.0" + "@oxfmt/binding-win32-x64-msvc" "0.65.0" oxlint-tsgolint@^7.0.2001: version "7.0.2001" @@ -14848,10 +14848,10 @@ unraw@^3.0.0: resolved "https://registry.yarnpkg.com/unraw/-/unraw-3.0.0.tgz#73443ed70d2ab09ccbac2b00525602d5991fbbe3" integrity sha512-08/DA66UF65OlpUDIQtbJyrqTR0jTAlJ+jsnkQ4jxR7+K5g5YG1APZKQSMCE1vqqmD+2pv6+IdEjmopFatacvg== -update-browserslist-db@^1.2.3: - version "1.2.3" - resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz#64d76db58713136acbeb4c49114366cc6cc2e80d" - integrity sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w== +update-browserslist-db@^1.3.0: + version "1.3.2" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.3.2.tgz#9d99fbff56c50bb11ba5fd35cece5916da595836" + integrity sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw== dependencies: escalade "^3.2.0" picocolors "^1.1.1" diff --git a/superset-frontend/cypress-base/package-lock.json b/superset-frontend/cypress-base/package-lock.json index cc5dd7612307..1b0ad723b6c4 100644 --- a/superset-frontend/cypress-base/package-lock.json +++ b/superset-frontend/cypress-base/package-lock.json @@ -3163,11 +3163,15 @@ ] }, "node_modules/baseline-browser-mapping": { - "version": "2.9.19", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", - "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==", + "version": "2.11.20", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.20.tgz", + "integrity": "sha512-H0ulySigv6icDJ1F7SjtdCD6PrhTpdYCmP0CactWy1+ekh0AFd0o1Wn5T8b+hnTmdBx19u9yhL6wvCylXMY7zw==", + "license": "Apache-2.0", "bin": { - "baseline-browser-mapping": "dist/cli.js" + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" } }, "node_modules/bcrypt-pbkdf": { @@ -3209,9 +3213,9 @@ } }, "node_modules/browserslist": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", - "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "version": "4.28.8", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz", + "integrity": "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==", "funding": [ { "type": "opencollective", @@ -3226,12 +3230,13 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.9.0", - "caniuse-lite": "^1.0.30001759", - "electron-to-chromium": "^1.5.263", - "node-releases": "^2.0.27", - "update-browserslist-db": "^1.2.0" + "baseline-browser-mapping": "^2.11.12", + "caniuse-lite": "^1.0.30001809", + "electron-to-chromium": "^1.5.402", + "node-releases": "^2.0.53", + "update-browserslist-db": "^1.3.0" }, "bin": { "browserslist": "cli.js" @@ -3325,9 +3330,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001769", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001769.tgz", - "integrity": "sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==", + "version": "1.0.30001810", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz", + "integrity": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==", "funding": [ { "type": "opencollective", @@ -3341,7 +3346,8 @@ "type": "github", "url": "https://github.com/sponsors/ai" } - ] + ], + "license": "CC-BY-4.0" }, "node_modules/caseless": { "version": "0.12.0", @@ -3997,9 +4003,10 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.286", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz", - "integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==" + "version": "1.5.418", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.418.tgz", + "integrity": "sha512-UzS26r3AEbG5wSoGVpJKqwHIU9zwQN7LHdVIThDrJpS0I5KdlXFMEb8543fhc9dVnIIAST6ar8rhwa00AL5MlA==", + "license": "ISC" }, "node_modules/emoji-regex": { "version": "8.0.0", @@ -4108,6 +4115,7 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", "engines": { "node": ">=6" } @@ -6630,9 +6638,13 @@ } }, "node_modules/node-releases": { - "version": "2.0.27", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", - "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==" + "version": "2.0.54", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.54.tgz", + "integrity": "sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } }, "node_modules/npm-run-path": { "version": "4.0.1", @@ -8344,9 +8356,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.2.tgz", + "integrity": "sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw==", "funding": [ { "type": "opencollective", @@ -8361,6 +8373,7 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" @@ -11118,9 +11131,9 @@ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" }, "baseline-browser-mapping": { - "version": "2.9.19", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", - "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==" + "version": "2.11.20", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.20.tgz", + "integrity": "sha512-H0ulySigv6icDJ1F7SjtdCD6PrhTpdYCmP0CactWy1+ekh0AFd0o1Wn5T8b+hnTmdBx19u9yhL6wvCylXMY7zw==" }, "bcrypt-pbkdf": { "version": "1.0.2", @@ -11156,15 +11169,15 @@ } }, "browserslist": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", - "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "version": "4.28.8", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz", + "integrity": "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==", "requires": { - "baseline-browser-mapping": "^2.9.0", - "caniuse-lite": "^1.0.30001759", - "electron-to-chromium": "^1.5.263", - "node-releases": "^2.0.27", - "update-browserslist-db": "^1.2.0" + "baseline-browser-mapping": "^2.11.12", + "caniuse-lite": "^1.0.30001809", + "electron-to-chromium": "^1.5.402", + "node-releases": "^2.0.53", + "update-browserslist-db": "^1.3.0" } }, "buffer-crc32": { @@ -11224,9 +11237,9 @@ "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" }, "caniuse-lite": { - "version": "1.0.30001769", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001769.tgz", - "integrity": "sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==" + "version": "1.0.30001810", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz", + "integrity": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==" }, "caseless": { "version": "0.12.0", @@ -11725,9 +11738,9 @@ } }, "electron-to-chromium": { - "version": "1.5.286", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz", - "integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==" + "version": "1.5.418", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.418.tgz", + "integrity": "sha512-UzS26r3AEbG5wSoGVpJKqwHIU9zwQN7LHdVIThDrJpS0I5KdlXFMEb8543fhc9dVnIIAST6ar8rhwa00AL5MlA==" }, "emoji-regex": { "version": "8.0.0", @@ -13470,9 +13483,9 @@ } }, "node-releases": { - "version": "2.0.27", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", - "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==" + "version": "2.0.54", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.54.tgz", + "integrity": "sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ==" }, "npm-run-path": { "version": "4.0.1", @@ -14699,9 +14712,9 @@ "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==" }, "update-browserslist-db": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.2.tgz", + "integrity": "sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw==", "requires": { "escalade": "^3.2.0", "picocolors": "^1.1.1" diff --git a/superset-frontend/package-lock.json b/superset-frontend/package-lock.json index a2e478f5e8d2..6e02bf39f7fd 100644 --- a/superset-frontend/package-lock.json +++ b/superset-frontend/package-lock.json @@ -16062,9 +16062,9 @@ } }, "node_modules/browserslist": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", - "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "version": "4.28.8", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz", + "integrity": "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==", "dev": true, "funding": [ { @@ -16082,11 +16082,11 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.9.0", - "caniuse-lite": "^1.0.30001759", - "electron-to-chromium": "^1.5.263", - "node-releases": "^2.0.27", - "update-browserslist-db": "^1.2.0" + "baseline-browser-mapping": "^2.11.12", + "caniuse-lite": "^1.0.30001809", + "electron-to-chromium": "^1.5.402", + "node-releases": "^2.0.53", + "update-browserslist-db": "^1.3.0" }, "bin": { "browserslist": "cli.js" @@ -16533,9 +16533,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001764", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001764.tgz", - "integrity": "sha512-9JGuzl2M+vPL+pz70gtMF9sHdMFbY9FJaQBi186cHKH3pSzDvzoUJUPV6fqiKIMyXbud9ZLg4F3Yza1vJ1+93g==", + "version": "1.0.30001810", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz", + "integrity": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==", "dev": true, "funding": [ { @@ -19371,9 +19371,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.267", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", - "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", + "version": "1.5.418", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.418.tgz", + "integrity": "sha512-UzS26r3AEbG5wSoGVpJKqwHIU9zwQN7LHdVIThDrJpS0I5KdlXFMEb8543fhc9dVnIIAST6ar8rhwa00AL5MlA==", "dev": true, "license": "ISC" }, @@ -30346,11 +30346,14 @@ } }, "node_modules/node-releases": { - "version": "2.0.27", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", - "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "version": "2.0.54", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.54.tgz", + "integrity": "sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=18" + } }, "node_modules/nomnom": { "version": "1.8.1", @@ -40405,9 +40408,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.2.tgz", + "integrity": "sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw==", "dev": true, "funding": [ { diff --git a/superset-frontend/src/SqlLab/components/SaveDatasetActionButton/SaveDatasetActionButton.test.tsx b/superset-frontend/src/SqlLab/components/SaveDatasetActionButton/SaveDatasetActionButton.test.tsx index 5df12e95a0c6..5266fa022308 100644 --- a/superset-frontend/src/SqlLab/components/SaveDatasetActionButton/SaveDatasetActionButton.test.tsx +++ b/superset-frontend/src/SqlLab/components/SaveDatasetActionButton/SaveDatasetActionButton.test.tsx @@ -43,6 +43,23 @@ describe('SaveDatasetActionButton', () => { expect(saveDatasetBtn).toBeVisible(); }); + test('disables only the dataset button when canSaveDataset is false', () => { + const onSaveAsExplore = jest.fn(); + render( + true} + onSaveAsExplore={onSaveAsExplore} + canSaveDataset={false} + />, + ); + + // Saving the query needs no results. + expect(screen.getByRole('button', { name: 'Save' })).toBeEnabled(); + expect( + screen.getByRole('button', { name: /save dataset/i }), + ).toBeDisabled(); + }); + test('disables the save dataset button when the query did not run successfully', async () => { render( { cleanup(); }); +afterEach(() => { + // In-body restores are skipped when an assertion throws, leaking a + // configured spy into later tests. + jest.restoreAllMocks(); +}); + // Mock createDatasource to return a thunk that resolves with the dataset's // new id. The test's mock store includes redux-thunk middleware (from RTK's // getDefaultMiddleware), so dispatch(createDatasource(...)) properly unwraps @@ -518,6 +526,39 @@ describe('SaveDatasetModal', () => { }); }); + test('surfaces the error and keeps the modal open when saving fails', async () => { + // The chart-payload step's toast was built but never dispatched, so a + // failure there was silent. + const postFormData = jest.spyOn( + require('src/explore/exploreUtils/formData'), + 'postFormData', + ); + postFormData.mockRejectedValue(new Error('Boom')); + const onHide = jest.fn(); + const store = createStore({ user }, reducerIndex); + + render(, { store }); + + fireEvent.change(screen.getByDisplayValue(/unimportant/i), { + target: { value: 'my dataset' }, + }); + userEvent.click(screen.getByRole('button', { name: /save/i })); + + // `createStore` builds its reducer map at runtime, so state isn't typed. + const toasts = () => + ( + store.getState() as unknown as { + messageToasts: { toastType: string }[]; + } + ).messageToasts; + + await waitFor(() => { + expect(toasts()).toHaveLength(1); + }); + expect(toasts()[0].toastType).toBe('DANGER_TOAST'); + expect(onHide).not.toHaveBeenCalled(); + }); + test('clearDatasetCache is imported and available', () => { const { clearDatasetCache } = require('src/utils/cachedSupersetGet'); diff --git a/superset-frontend/src/SqlLab/components/SaveDatasetModal/index.tsx b/superset-frontend/src/SqlLab/components/SaveDatasetModal/index.tsx index 38e4050a6f80..78606cfa790f 100644 --- a/superset-frontend/src/SqlLab/components/SaveDatasetModal/index.tsx +++ b/superset-frontend/src/SqlLab/components/SaveDatasetModal/index.tsx @@ -61,6 +61,9 @@ import type Subject from 'src/types/Subject'; import { openInNewTab, redirect } from 'src/utils/navigationUtils'; import { mapSubjectValuesToIds } from 'src/features/subjects/SubjectPicker'; +// Derived so it can't drift from what `getClientErrorObject` accepts. +type SaveErrorSource = Parameters[0]; + interface QueryDatabase { id?: number; } @@ -391,9 +394,18 @@ export const SaveDatasetModal = ({ setDatasetName(getDefaultDatasetName()); onHide(); }) - .catch(() => { + .catch((error?: SaveErrorSource) => { setLoading(false); - addDangerToast(t('An error occurred saving dataset')); + // `createDatasource` already toasted the server's message and rejects + // with nothing; only the chart-payload step needs its own. + if (!error) { + return; + } + getClientErrorObject(error).then(e => + dispatch( + addDangerToast(e.error || t('An error occurred saving dataset')), + ), + ); }); }; diff --git a/superset-frontend/src/SqlLab/components/SaveQuery/SaveQuery.test.tsx b/superset-frontend/src/SqlLab/components/SaveQuery/SaveQuery.test.tsx index 20b40792fb54..0cb0ec73504b 100644 --- a/superset-frontend/src/SqlLab/components/SaveQuery/SaveQuery.test.tsx +++ b/superset-frontend/src/SqlLab/components/SaveQuery/SaveQuery.test.tsx @@ -27,6 +27,8 @@ import { import SaveQuery from 'src/SqlLab/components/SaveQuery'; import { initialState, databases } from 'src/SqlLab/fixtures'; +const RESULT_COLUMNS = [{ column_name: 'col', type: 'STRING' }]; + const mockedProps = { queryEditorId: '123', animation: false, @@ -35,7 +37,6 @@ const mockedProps = { onSave: () => {}, saveQueryWarning: null, columns: [], - canSaveDataset: true, }; const mockState = { @@ -60,8 +61,31 @@ const splitSaveBtnProps = { ...mockedProps.database, allows_virtual_table_explore: true, }, + columns: RESULT_COLUMNS, }; +const EDITOR_SQL = 'SELECT * FROM t'; + +const stateWithLatestQuery = ({ + id, + state, + sql = EDITOR_SQL, +}: { + id: string; + state: string; + sql?: string; +}) => ({ + ...mockState, + sqlLab: { + ...mockState.sqlLab, + queryEditors: mockState.sqlLab.queryEditors.map(qe => ({ + ...qe, + latestQueryId: id, + })), + queries: { [id]: { id, state, sql } }, + }, +}); + const middlewares = [thunk]; const mockStore = configureStore(middlewares); @@ -97,6 +121,71 @@ describe('SavedQuery', () => { expect(saveBtn).toBeVisible(); }); + test('blocks "Save dataset" until the query has run successfully', () => { + // Without a successful run the save can only fail server-side. + render(, { + useRedux: true, + store: mockStore(stateWithLatestQuery({ id: 'qid-1', state: 'failed' })), + }); + + expect( + screen.getByRole('button', { name: /save dataset/i }), + ).toBeDisabled(); + // Saving the query itself is unaffected. + expect(screen.getByRole('button', { name: 'Save' })).toBeEnabled(); + }); + + test('blocks "Save dataset" when no query has been run at all', () => { + render(, { + useRedux: true, + store: mockStore(mockState), + }); + + expect( + screen.getByRole('button', { name: /save dataset/i }), + ).toBeDisabled(); + }); + + test('blocks "Save dataset" when the SQL changed after a successful run', () => { + // The run succeeded, but not for what is in the editor now -- and it is + // the editor's SQL that gets saved. + render(, { + useRedux: true, + store: mockStore( + stateWithLatestQuery({ + id: 'qid-1', + state: 'success', + sql: 'SELECT 1 AS ran_earlier', + }), + ), + }); + + expect( + screen.getByRole('button', { name: /save dataset/i }), + ).toBeDisabled(); + }); + + test('blocks "Save dataset" when the successful query returned no columns', () => { + // e.g. a DDL/DML statement -- there is nothing to introspect into a dataset. + render(, { + useRedux: true, + store: mockStore(stateWithLatestQuery({ id: 'qid-1', state: 'success' })), + }); + + expect( + screen.getByRole('button', { name: /save dataset/i }), + ).toBeDisabled(); + }); + + test('enables "Save dataset" once the query has succeeded', () => { + render(, { + useRedux: true, + store: mockStore(stateWithLatestQuery({ id: 'qid-1', state: 'success' })), + }); + + expect(screen.getByRole('button', { name: /save dataset/i })).toBeEnabled(); + }); + test('renders a save query modal when user clicks save button', () => { render(, { useRedux: true, @@ -234,7 +323,7 @@ describe('SavedQuery', () => { test('renders a save dataset modal when user clicks "save dataset" menu item', async () => { render(, { useRedux: true, - store: mockStore(mockState), + store: mockStore(stateWithLatestQuery({ id: 'qid-1', state: 'success' })), }); const saveDatasetMenuItem = await screen.findByLabelText(/save dataset/i); @@ -248,7 +337,7 @@ describe('SavedQuery', () => { test('renders the save dataset modal UI', async () => { render(, { useRedux: true, - store: mockStore(mockState), + store: mockStore(stateWithLatestQuery({ id: 'qid-1', state: 'success' })), }); const saveDatasetMenuItem = await screen.findByLabelText(/save dataset/i); userEvent.click(saveDatasetMenuItem); diff --git a/superset-frontend/src/SqlLab/components/SaveQuery/index.tsx b/superset-frontend/src/SqlLab/components/SaveQuery/index.tsx index 81a493df6218..dccdb3b497af 100644 --- a/superset-frontend/src/SqlLab/components/SaveQuery/index.tsx +++ b/superset-frontend/src/SqlLab/components/SaveQuery/index.tsx @@ -17,6 +17,8 @@ * under the License. */ import { useState, useEffect, useMemo, ChangeEvent } from 'react'; +import { useSelector } from 'react-redux'; +import { Query, QueryState } from '@superset-ui/core'; import type { DatabaseObject } from 'src/features/databases/types'; import { t } from '@apache-superset/core/translation'; import { styled } from '@apache-superset/core/theme'; @@ -37,7 +39,7 @@ import { } from 'src/SqlLab/components/SaveDatasetModal'; import { getDatasourceAsSaveableDataset } from 'src/utils/datasourceUtils'; import useQueryEditor from 'src/SqlLab/hooks/useQueryEditor'; -import { QueryEditor } from 'src/SqlLab/types'; +import { QueryEditor, SqlLabRootState } from 'src/SqlLab/types'; import useLogAction from 'src/logger/useLogAction'; import { LOG_ACTIONS_SQLLAB_CREATE_CHART, @@ -52,7 +54,6 @@ interface SaveQueryProps { onUpdate: (arg0: QueryPayload, id: string) => void; saveQueryWarning: string | null; database: Partial | undefined; - canSaveDataset: boolean; } export type QueryPayload = { @@ -82,7 +83,6 @@ const SaveQuery = ({ saveQueryWarning, database, columns, - canSaveDataset, }: SaveQueryProps) => { const queryEditor = useQueryEditor(queryEditorId, [ 'autorun', @@ -113,6 +113,17 @@ const SaveQuery = ({ const [label, setLabel] = useState(defaultLabel); const [showSave, setShowSave] = useState(false); const [showSaveDatasetModal, setShowSaveDatasetModal] = useState(false); + // Saving a dataset runs the SQL to introspect columns, so it needs a + // successful run of the SQL being saved that produced at least one column + // -- editing after a run invalidates it, and running a selection only + // validates that selection. + const latestQuery = useSelector( + ({ sqlLab }) => sqlLab.queries[queryEditor.latestQueryId || ''], + ); + const canSaveDataset = + latestQuery?.state === QueryState.Success && + latestQuery.sql === queryEditor.sql && + columns.length > 0; const isSaved = !!query.remoteId; const isLabelEmpty = label.trim().length === 0; const canExploreDatabase = !!database?.allows_virtual_table_explore; diff --git a/superset-frontend/src/SqlLab/components/SqlEditor/SqlEditor.test.tsx b/superset-frontend/src/SqlLab/components/SqlEditor/SqlEditor.test.tsx index 116912d04973..d2bd1380ad4c 100644 --- a/superset-frontend/src/SqlLab/components/SqlEditor/SqlEditor.test.tsx +++ b/superset-frontend/src/SqlLab/components/SqlEditor/SqlEditor.test.tsx @@ -362,6 +362,7 @@ describe('SqlEditor', () => { test('enables the save dataset button when the latest query succeeded', async () => { const { findByLabelText } = setupWithLatestQuery({ state: QueryState.Success, + sql: mockedProps.queryEditor.sql, }); expect(await findByLabelText('Save dataset')).toBeEnabled(); }); diff --git a/superset-frontend/src/SqlLab/components/SqlEditor/index.tsx b/superset-frontend/src/SqlLab/components/SqlEditor/index.tsx index 9efe18c8a5ce..5d4a6f567edb 100644 --- a/superset-frontend/src/SqlLab/components/SqlEditor/index.tsx +++ b/superset-frontend/src/SqlLab/components/SqlEditor/index.tsx @@ -868,7 +868,6 @@ const SqlEditor: FC = ({ } saveQueryWarning={saveQueryWarning} database={database} - canSaveDataset={successful && resultColumns.length > 0} /> diff --git a/superset/commands/dataset/create.py b/superset/commands/dataset/create.py index 310fa9b24d83..12084649ec4b 100644 --- a/superset/commands/dataset/create.py +++ b/superset/commands/dataset/create.py @@ -33,7 +33,18 @@ ) from superset.commands.utils import populate_subjects from superset.daos.dataset import DatasetDAO -from superset.exceptions import SupersetParseError, SupersetSecurityException +from superset.db_engine_specs.exceptions import ( + SupersetDBAPIConnectionError, + SupersetDBAPIDatabaseError, + SupersetDBAPIOperationalError, +) +from superset.exceptions import ( + OAuth2RedirectError, + SupersetException, + SupersetParseError, + SupersetSecurityException, + SupersetTimeoutException, +) from superset.extensions import security_manager from superset.sql.parse import Table from superset.utils.decorators import on_error, transaction @@ -50,7 +61,38 @@ def run(self) -> Model: self.validate() dataset = DatasetDAO.create(attributes=self._properties) - dataset.fetch_metadata() + try: + dataset.fetch_metadata() + except OAuth2RedirectError: + # Must reach the caller unchanged to start the OAuth2 dance. + raise + except ( + SupersetTimeoutException, + SupersetDBAPIConnectionError, + SupersetDBAPIOperationalError, + SupersetDBAPIDatabaseError, + ): + # Infra-level failures (unreachable database, query timeout), not + # bad user input: let them propagate with their own status + # instead of being coerced into a 422 "invalid table" error. + raise + except SupersetException as ex: + # Not a SQLAlchemyError, so ``on_error`` re-raises it untouched and + # it escapes to FAB's ``@safe`` as an opaque 500 "Fatal error". + # Deliberately covers the 403 ``SupersetSecurityException`` raised + # for mutation/multi-statement SQL too: ``validate()`` already + # reports that class of rejection as a 422 on ``sql`` via + # ``DatasetDataAccessIsNotAllowed``. + raise DatasetInvalidError( + exceptions=[ + ValidationError( + # ``lazy_gettext`` messages aren't ``str``, so + # marshmallow won't wrap them into a list on its own. + [str(ex.message)], + field_name="sql" if self._properties.get("sql") else "table", + ) + ] + ) from ex return dataset def validate(self) -> None: # noqa: C901 diff --git a/superset/connectors/sqla/utils.py b/superset/connectors/sqla/utils.py index 67dde802b4ea..a8894d9f25ea 100644 --- a/superset/connectors/sqla/utils.py +++ b/superset/connectors/sqla/utils.py @@ -32,8 +32,10 @@ from superset import db from superset.constants import LRU_CACHE_MAX_SIZE +from superset.db_engine_specs.exceptions import SupersetDBAPIError from superset.errors import ErrorLevel, SupersetError, SupersetErrorType from superset.exceptions import ( + SupersetErrorException, SupersetGenericDBErrorException, SupersetParseError, SupersetSecurityException, @@ -125,12 +127,14 @@ def get_virtual_table_metadata(dataset: SqlaTable) -> list[ResultSetColumnType]: # rest (sandbox violations, malformed template syntax, encoding # errors) indicate a real problem with the template that must # surface. See #38012. + # str(ex) stringifies the raw SupersetError list (enum reprs and all). + error_message = "; ".join(err.message for err in ex.errors) if isinstance(ex.__cause__, UndefinedError): raise SupersetVirtualTableParseException( - message=_("Template processing error: %(error)s", error=str(ex)), + message=_("Template processing error: %(error)s", error=error_message), ) from ex raise SupersetGenericDBErrorException( - message=_("Template processing error: %(error)s", error=str(ex)), + message=_("Template processing error: %(error)s", error=error_message), ) from ex try: parsed_script = SQLScript(sql, engine=db_engine_spec.engine) @@ -209,6 +213,13 @@ def get_columns_description( result, cursor.description, db_engine_spec ) return result_set.columns + except (SupersetErrorException, SupersetDBAPIError): + # Preserve exceptions that already carry specific, actionable typing + # (e.g. OAuth2RedirectError, or a driver connection/timeout error + # normalized by ``db_engine_spec.execute`` via its ``custom_errors`` + # mapping) so callers can act on them instead of seeing an opaque + # generic DB error. + raise except Exception as ex: raise SupersetGenericDBErrorException(message=str(ex)) from ex diff --git a/superset/datasets/api.py b/superset/datasets/api.py index 44ced275022d..e65cc4a83d85 100644 --- a/superset/datasets/api.py +++ b/superset/datasets/api.py @@ -87,8 +87,10 @@ openapi_spec_methods_override, ) from superset.exceptions import ( + OAuth2RedirectError, SupersetSyntaxErrorException, SupersetTemplateException, + SupersetTimeoutException, ) from superset.jinja_context import BaseTemplateProcessor, get_template_processor from superset.subjects.filters import FilterRelatedSubjects, subject_type_filter @@ -459,7 +461,6 @@ def get_list_headless(self, **kwargs: Any) -> Response: @expose("/", methods=("POST",)) @protect() - @safe @statsd_metrics @event_logger.log_this_with_context( action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.post", @@ -514,6 +515,12 @@ def post(self) -> Response: data=new_model.data, uuid=new_model.uuid, ) + except OAuth2RedirectError: + # Must reach the client unchanged to start the OAuth2 dance; + # ``@safe`` isn't used on this endpoint since it would otherwise + # swallow this into an opaque 500 that drops the ``url``/``tab_id`` + # extras the frontend needs. + raise except DatasetSoftDeletedTwinExistsError as ex: return self.response_422(message=str(ex)) except DatasetInvalidError as ex: @@ -526,6 +533,24 @@ def post(self) -> Response: exc_info=True, ) return self.response_422(message=str(ex)) + except SupersetTimeoutException as ex: + # ``CreateDatasetCommand`` deliberately re-raises this, along with + # other infra-level failures, unchanged instead of coercing it + # into a 422 "invalid table"/"invalid sql" error. Of that group, + # only the timeout has a status (408) worth surfacing distinctly; + # the ``SupersetDBAPIError`` subclasses inherit the base class's + # 500 and fall through to the broad ``except Exception`` below so + # their raw driver/connection text isn't echoed to the client. + logger.warning("Error creating dataset: %s", ex, exc_info=True) + return self.response(ex.status, message=str(ex)) + except Exception: # pylint: disable=broad-except + # ``@safe`` isn't used on this endpoint (it would swallow the + # ``OAuth2RedirectError`` re-raised above into an opaque 500), so + # replicate its behavior here for any other unexpected exception: + # log the full error server-side, but don't echo internal details + # (ORM/driver error text, connection info) back to the caller. + logger.exception("Unexpected error in DatasetRestApi.post") + return self.response_500(message="Fatal error") @expose("/", methods=("PUT",)) @protect() diff --git a/superset/mcp_service/dataset/tool/create_virtual_dataset.py b/superset/mcp_service/dataset/tool/create_virtual_dataset.py index f231a0192580..31eb795a4565 100644 --- a/superset/mcp_service/dataset/tool/create_virtual_dataset.py +++ b/superset/mcp_service/dataset/tool/create_virtual_dataset.py @@ -220,6 +220,8 @@ async def create_virtual_dataset( # noqa: C901 error=f"Failed to update dataset metadata (creation rolled back): {exc}", ) except SupersetGenericDBErrorException as exc: + # Defensive backstop for direct raises (see + # test_create_virtual_dataset_sql_error_is_actionable). logger.warning("Virtual dataset SQL validation failed", exc_info=True) await ctx.warning(f"Virtual dataset SQL failed validation: {exc}") return CreateVirtualDatasetResponse( diff --git a/tests/unit_tests/commands/dataset/test_create.py b/tests/unit_tests/commands/dataset/test_create.py index 75174cf7505b..c631c2711d9b 100644 --- a/tests/unit_tests/commands/dataset/test_create.py +++ b/tests/unit_tests/commands/dataset/test_create.py @@ -18,11 +18,18 @@ import pytest from marshmallow import ValidationError +from pytest_mock import MockerFixture from superset.commands.dataset.create import CreateDatasetCommand from superset.commands.dataset.exceptions import DatasetInvalidError +from superset.db_engine_specs.exceptions import SupersetDBAPIConnectionError from superset.errors import ErrorLevel, SupersetError, SupersetErrorType -from superset.exceptions import SupersetParseError +from superset.exceptions import ( + OAuth2RedirectError, + SupersetGenericDBErrorException, + SupersetParseError, + SupersetTimeoutException, +) from superset.models.core import Database @@ -250,3 +257,161 @@ def test_create_dataset_generic_exists_error_when_no_twin() -> None: ) with pytest.raises(DatasetInvalidError): command.validate() + + +def test_create_dataset_metadata_fetch_error_is_structured( + mocker: MockerFixture, +) -> None: + """A metadata-fetch failure must surface the engine's own message. + + ``run()`` executes the SQL to introspect columns; the resulting + ``SupersetGenericDBErrorException`` used to escape as a 500 "Fatal error". + """ + mocker.patch.object(CreateDatasetCommand, "validate") + dataset = Mock() + dataset.fetch_metadata.side_effect = SupersetGenericDBErrorException( + message="Invalid SQL: Unable to parse: SELECT ...", + ) + mocker.patch( + "superset.commands.dataset.create.DatasetDAO.create", + return_value=dataset, + ) + + command = CreateDatasetCommand( + { + "database": 1, + "table_name": "dataset wrong", + "sql": "SELECT ...", + } + ) + + with pytest.raises(DatasetInvalidError) as exc_info: + command.run() + + validation_errors = exc_info.value._exceptions + assert len(validation_errors) == 1 + assert validation_errors[0].field_name == "sql" + assert "Invalid SQL: Unable to parse: SELECT ..." in str( + validation_errors[0].messages[0] + ) + + +def test_create_dataset_metadata_fetch_error_physical_table( + mocker: MockerFixture, +) -> None: + """The same conversion applies to physical datasets, keyed on ``table``.""" + mocker.patch.object(CreateDatasetCommand, "validate") + dataset = Mock() + dataset.fetch_metadata.side_effect = SupersetGenericDBErrorException( + message="(psycopg2.OperationalError) could not connect to server", + ) + mocker.patch( + "superset.commands.dataset.create.DatasetDAO.create", + return_value=dataset, + ) + + command = CreateDatasetCommand({"database": 1, "table_name": "physical_table"}) + + with pytest.raises(DatasetInvalidError) as exc_info: + command.run() + + validation_errors = exc_info.value._exceptions + assert validation_errors[0].field_name == "table" + assert "could not connect to server" in str(validation_errors[0].messages[0]) + + +def test_create_dataset_oauth2_redirect_propagates_unchanged( + mocker: MockerFixture, +) -> None: + """OAuth2 redirects must not be flattened into a DatasetInvalidError.""" + mocker.patch.object(CreateDatasetCommand, "validate") + dataset = Mock() + oauth2_error = OAuth2RedirectError( + url="https://example.org/oauth2/authorize", + tab_id="tab-123", + redirect_uri="https://superset.example.org/oauth2/redirect", + ) + dataset.fetch_metadata.side_effect = oauth2_error + mocker.patch( + "superset.commands.dataset.create.DatasetDAO.create", + return_value=dataset, + ) + + command = CreateDatasetCommand( + {"database": 1, "table_name": "good_dataset", "sql": "SELECT 1 AS a"} + ) + + with pytest.raises(OAuth2RedirectError) as exc_info: + command.run() + + assert exc_info.value is oauth2_error + assert exc_info.value.error.extra["url"] == "https://example.org/oauth2/authorize" + assert exc_info.value.error.extra["tab_id"] == "tab-123" + + +def test_create_dataset_timeout_propagates_unchanged( + mocker: MockerFixture, +) -> None: + """A query timeout is an infra failure, not bad user input: it must not + be flattened into a 422 DatasetInvalidError on ``table``/``sql``.""" + mocker.patch.object(CreateDatasetCommand, "validate") + dataset = Mock() + timeout_error = SupersetTimeoutException( + error_type=SupersetErrorType.CONNECTION_DATABASE_TIMEOUT, + message="Connection timed out", + level=ErrorLevel.ERROR, + ) + dataset.fetch_metadata.side_effect = timeout_error + mocker.patch( + "superset.commands.dataset.create.DatasetDAO.create", + return_value=dataset, + ) + + command = CreateDatasetCommand({"database": 1, "table_name": "physical_table"}) + + with pytest.raises(SupersetTimeoutException) as exc_info: + command.run() + + assert exc_info.value is timeout_error + + +def test_create_dataset_connection_error_propagates_unchanged( + mocker: MockerFixture, +) -> None: + """An unreachable database must not be reported as an invalid table name.""" + mocker.patch.object(CreateDatasetCommand, "validate") + dataset = Mock() + connection_error = SupersetDBAPIConnectionError( + "could not connect to server: Connection refused" + ) + dataset.fetch_metadata.side_effect = connection_error + mocker.patch( + "superset.commands.dataset.create.DatasetDAO.create", + return_value=dataset, + ) + + command = CreateDatasetCommand({"database": 1, "table_name": "physical_table"}) + + with pytest.raises(SupersetDBAPIConnectionError) as exc_info: + command.run() + + assert exc_info.value is connection_error + + +def test_create_dataset_run_succeeds_when_metadata_fetch_works( + mocker: MockerFixture, +) -> None: + """Control: the happy path still returns the created dataset.""" + mocker.patch.object(CreateDatasetCommand, "validate") + dataset = Mock() + mocker.patch( + "superset.commands.dataset.create.DatasetDAO.create", + return_value=dataset, + ) + + command = CreateDatasetCommand( + {"database": 1, "table_name": "good_dataset", "sql": "SELECT 1 AS a"} + ) + + assert command.run() is dataset + dataset.fetch_metadata.assert_called_once() diff --git a/tests/unit_tests/connectors/sqla/test_utils.py b/tests/unit_tests/connectors/sqla/test_utils.py index c10fe2b3d028..ad684ff78878 100644 --- a/tests/unit_tests/connectors/sqla/test_utils.py +++ b/tests/unit_tests/connectors/sqla/test_utils.py @@ -191,6 +191,39 @@ def test_get_virtual_table_metadata_template_security_error_is_not_softened(): assert "Template processing error" in str(exc_info.value.message) +def test_get_virtual_table_metadata_template_error_message_is_clean(): + """The message must be the SupersetError's own text, not str(ex).""" + mock_dataset = Mock(spec=SqlaTable) + mock_database = Mock(spec=Database) + mock_dataset.database = mock_database + mock_dataset.sql = "SELECT 1 {% if %}" + + ex = SupersetSyntaxErrorException( + [ + SupersetError( + message="Malformed template, expected 'endif'", + error_type=SupersetErrorType.GENERIC_COMMAND_ERROR, + level=ErrorLevel.ERROR, + ) + ] + ) + ex.__cause__ = SecurityError("unrelated cause, not UndefinedError") + mock_template_processor = Mock() + mock_template_processor.process_template.side_effect = ex + mock_dataset.get_template_processor.return_value = mock_template_processor + mock_dataset.template_params_dict = {} + + with pytest.raises(SupersetGenericDBErrorException) as exc_info: + get_virtual_table_metadata(mock_dataset) + + message = str(exc_info.value.message) + assert message == ( + "Template processing error: Malformed template, expected 'endif'" + ) + assert "SupersetError(" not in message + assert "error_type=<" not in message + + def test_get_virtual_table_metadata_multiple_statements_not_allowed(): """Test that multiple SQL statements raise security error.""" mock_dataset = Mock(spec=SqlaTable) diff --git a/tests/unit_tests/connectors/sqla/utils_test.py b/tests/unit_tests/connectors/sqla/utils_test.py index eb404ffedf3a..13753817f31e 100644 --- a/tests/unit_tests/connectors/sqla/utils_test.py +++ b/tests/unit_tests/connectors/sqla/utils_test.py @@ -26,7 +26,8 @@ get_columns_description, get_virtual_table_metadata, ) -from superset.exceptions import SupersetSecurityException +from superset.db_engine_specs.exceptions import SupersetDBAPIConnectionError +from superset.exceptions import OAuth2RedirectError, SupersetSecurityException from superset.models.core import Database @@ -102,6 +103,51 @@ def test_returns_column_descriptions(mocker: MockerFixture) -> None: ] +def test_get_columns_description_propagates_oauth2_redirect( + mocker: MockerFixture, +) -> None: + """ + ``get_columns_description`` wraps every exception raised while executing + the metadata query into ``SupersetGenericDBErrorException`` -- but an + ``OAuth2RedirectError`` raised by the driver (e.g. a database requiring + per-user OAuth2 tokens) must reach the caller unchanged so the frontend + can start the OAuth2 dance, instead of being flattened into an opaque + generic DB error. + """ + database = mocker.MagicMock() + cursor = mocker.MagicMock() + oauth2_error = OAuth2RedirectError("https://example.org/oauth2", "tab-id", "uri") + + database.get_raw_connection.return_value.__enter__.return_value.cursor.return_value = cursor # noqa: E501 + database.db_engine_spec.execute.side_effect = oauth2_error + + with pytest.raises(OAuth2RedirectError): + get_columns_description(database, "catalog", "schema", "SELECT * FROM table") + + +def test_get_columns_description_propagates_dbapi_error( + mocker: MockerFixture, +) -> None: + """ + A connection failure normalized by ``db_engine_spec.execute`` into a + ``SupersetDBAPIConnectionError`` must also reach the caller unchanged -- + not be re-flattened into an opaque ``SupersetGenericDBErrorException`` -- + so callers like ``CreateDatasetCommand`` can let it propagate with its + own status instead of misreporting an infra failure as bad user input. + """ + database = mocker.MagicMock() + cursor = mocker.MagicMock() + connection_error = SupersetDBAPIConnectionError( + "could not connect to server: Connection refused" + ) + + database.get_raw_connection.return_value.__enter__.return_value.cursor.return_value = cursor # noqa: E501 + database.db_engine_spec.execute.side_effect = connection_error + + with pytest.raises(SupersetDBAPIConnectionError): + get_columns_description(database, "catalog", "schema", "SELECT * FROM table") + + def _create_zero_row_database(tmp_path: Path) -> tuple[Database, str]: """ Create a real SQLite-backed ``Database`` and a query that matches zero rows. diff --git a/tests/unit_tests/datasets/api_tests.py b/tests/unit_tests/datasets/api_tests.py index 2d676c458700..002ae6d8d1aa 100644 --- a/tests/unit_tests/datasets/api_tests.py +++ b/tests/unit_tests/datasets/api_tests.py @@ -231,6 +231,223 @@ def _create_dataset(name: str) -> Any: return dataset +def test_post_dataset_with_invalid_sql_returns_actionable_422( + session: Session, + client: Any, + full_api_access: None, +) -> None: + """Saving a dataset over unrunnable SQL must explain what is wrong. + + With blanket database access ``validate()`` never parses the SQL, so + ``run()``'s column introspection is the first thing to reject it. That + used to surface as a bare 500 ``{"message": "Fatal error"}``. + """ + from superset.connectors.sqla.models import SqlaTable + from superset.models.core import Database + + SqlaTable.metadata.create_all(db.session.get_bind()) + + database = Database(database_name="invalid_sql_db", sqlalchemy_uri="sqlite://") + db.session.add(database) + db.session.flush() + + response = client.post( + "/api/v1/dataset/", + json={ + "database": database.id, + "schema": "main", + "table_name": "dataset wrong", + "sql": "SELECT ...", + }, + ) + + assert response.status_code == 422 + message = response.json["message"] + assert "Fatal error" not in str(message) + # Not the parser's exact wording -- that would break on a sqlglot bump. + assert message["sql"][0].startswith("Invalid SQL") + + # The failed create must not leave a half-built dataset behind. + assert ( + db.session.query(SqlaTable).filter_by(table_name="dataset wrong").one_or_none() + is None + ) + + +def test_post_dataset_oauth2_redirect_propagates_unchanged( + session: Session, + client: Any, + full_api_access: None, +) -> None: + """OAuth2RedirectError must reach the client with its ``url``/``tab_id`` + extras intact so the frontend can start the OAuth2 dance. + + ``DatasetRestApi.post`` doesn't use flask-appbuilder's ``@safe`` + decorator for this reason: ``@safe`` catches any uncaught exception and + flattens it into an opaque 500, which would strip those extras. + """ + from superset.connectors.sqla.models import SqlaTable + from superset.exceptions import OAuth2RedirectError + from superset.models.core import Database + + SqlaTable.metadata.create_all(db.session.get_bind()) + + database = Database(database_name="oauth2_db", sqlalchemy_uri="sqlite://") + db.session.add(database) + db.session.flush() + + with patch( + "superset.datasets.api.CreateDatasetCommand.run", + side_effect=OAuth2RedirectError( + "http://example.org/auth", "tab-1", "/redirect" + ), + ): + response = client.post( + "/api/v1/dataset/", + json={ + "database": database.id, + "schema": "main", + "table_name": "oauth2_table", + }, + ) + + assert response.status_code == 403 + error = response.json["errors"][0] + assert error["error_type"] == "OAUTH2_REDIRECT" + assert error["extra"] == { + "url": "http://example.org/auth", + "tab_id": "tab-1", + "redirect_uri": "/redirect", + } + + +def test_post_dataset_timeout_propagates_own_status( + session: Session, + client: Any, + full_api_access: None, +) -> None: + """A query timeout while fetching metadata must surface as its own 408, + not be flattened into the catch-all 500 "Fatal error" -- the same class + of failure ``CreateDatasetCommand`` deliberately re-raises unchanged so + it isn't misreported as a 422 "invalid table"/"invalid sql" error. + """ + from superset.connectors.sqla.models import SqlaTable + from superset.errors import ErrorLevel, SupersetErrorType + from superset.exceptions import SupersetTimeoutException + from superset.models.core import Database + + SqlaTable.metadata.create_all(db.session.get_bind()) + + database = Database(database_name="timeout_db", sqlalchemy_uri="sqlite://") + db.session.add(database) + db.session.flush() + + with patch( + "superset.datasets.api.CreateDatasetCommand.run", + side_effect=SupersetTimeoutException( + error_type=SupersetErrorType.CONNECTION_DATABASE_TIMEOUT, + message="Connection timed out", + level=ErrorLevel.ERROR, + ), + ): + response = client.post( + "/api/v1/dataset/", + json={ + "database": database.id, + "schema": "main", + "table_name": "timeout_table", + }, + ) + + assert response.status_code == 408 + assert "Connection timed out" in response.json["message"] + + +def test_post_dataset_dbapi_connection_error_returns_sanitized_500( + session: Session, + client: Any, + full_api_access: None, +) -> None: + """Unlike ``SupersetTimeoutException``, ``SupersetDBAPIConnectionError`` and + its sibling ``SupersetDBAPIError`` subclasses (aside from + ``SupersetDBAPIProgrammingError``) inherit the base 500 status, so there is + no distinct status to surface for them. They must fall through to the same + sanitized "Fatal error" response as any other unexpected exception, not + echo the raw driver/connection text ``CreateDatasetCommand`` re-raises them + with. + """ + from superset.connectors.sqla.models import SqlaTable + from superset.db_engine_specs.exceptions import SupersetDBAPIConnectionError + from superset.models.core import Database + + SqlaTable.metadata.create_all(db.session.get_bind()) + + database = Database(database_name="dbapi_error_db", sqlalchemy_uri="sqlite://") + db.session.add(database) + db.session.flush() + + secret = "postgresql://admin:s3cr3t@internal-db.example.com/prod" # noqa: S105 + with patch( + "superset.datasets.api.CreateDatasetCommand.run", + side_effect=SupersetDBAPIConnectionError(secret), + ): + response = client.post( + "/api/v1/dataset/", + json={ + "database": database.id, + "schema": "main", + "table_name": "dbapi_error_table", + }, + ) + + assert response.status_code == 500 + assert response.json == {"message": "Fatal error"} + assert secret not in response.get_data(as_text=True) + + +def test_post_dataset_unexpected_error_returns_sanitized_500( + session: Session, + client: Any, + full_api_access: None, +) -> None: + """An unexpected, non-``SupersetException`` failure must be flattened + into an opaque 500 -- the same contract ``@safe`` used to provide -- + instead of leaking raw exception text (e.g. driver/connection details) + through Flask's catch-all error handler. + + ``DatasetRestApi.post`` doesn't use ``@safe`` so that ``OAuth2RedirectError`` + can reach the client unchanged (see + ``test_post_dataset_oauth2_redirect_propagates_unchanged``); it must + replicate ``@safe``'s opaque-500 behavior itself for everything else. + """ + from superset.connectors.sqla.models import SqlaTable + from superset.models.core import Database + + SqlaTable.metadata.create_all(db.session.get_bind()) + + database = Database(database_name="unexpected_db", sqlalchemy_uri="sqlite://") + db.session.add(database) + db.session.flush() + + secret = "postgresql://admin:s3cr3t@internal-db.example.com/prod" # noqa: S105 + with patch( + "superset.datasets.api.CreateDatasetCommand.run", + side_effect=RuntimeError(secret), + ): + response = client.post( + "/api/v1/dataset/", + json={ + "database": database.id, + "schema": "main", + "table_name": "unexpected_table", + }, + ) + + assert response.status_code == 500 + assert response.json == {"message": "Fatal error"} + assert secret not in response.get_data(as_text=True) + + def test_put_dataset_rejects_stale_if_match( session: Session, client: Any,