Compare commits

...

18 Commits

Author SHA1 Message Date
Joe Li
76dde24b7a style(dashboard): wrap topLevelTabs locator to satisfy prettier
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 14:19:30 +00:00
Joe Li
760084c631 test(dashboard): scope top-level tab locator and use typed dataset helper
- topLevelTabs() now scopes the nav list to the immediate child (:scope >)
  so tabs from nested tab bars rendered inside the container content are not
  counted by .count()/.nth() callers.
- Replace local findDatasetIdByName(page: any) with the typed
  getDatasetByName(page: Page) helper (retry-wrapped apiGet, no new any).
Addresses review feedback.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 14:19:30 +00:00
Joe Li
415cc534cc test(dashboard): migrate tabs resize E2E to Playwright
Migrate the genuinely end-to-end case from the deprecated Cypress
"Dashboard tabs" suite to the Playwright framework: a chart in a hidden
tab must re-measure and refit its container when the tab is revealed
after the available width changed.

The dashboard is built hermetically (two top-level tabs, a width-
sensitive treemap in the first) via the API and the viewport is shrunk
while the treemap is hidden — replacing the original's dependency on a
seeded tabbed dashboard and native-filter-bar expansion. The test
asserts both that the container actually reflowed (guarding against a
trivial green) and that the chart refit with no horizontal overflow.

The other two original cases asserted only the active-tab CSS class and
query-on-visible behaviour (one already skipped); those are DOM/state
assertions with no backend invariant and are intentionally not migrated.

Adds DashboardPage.topLevelTabs()/switchToTopLevelTab() helpers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 14:19:21 +00:00
Evan Rusackas
d5e75c4813 fix(ci): correct setup-node version comment in tag-release.yml (#42039) 2026-07-14 19:43:13 +07:00
dependabot[bot]
cc32d16e0c chore(deps-dev): bump @typescript-eslint/parser from 8.62.0 to 8.63.0 in /superset-websocket (#42021)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-14 05:14:28 -07:00
dependabot[bot]
258f4c035b chore(deps-dev): bump typescript-eslint from 8.62.1 to 8.63.0 in /superset-websocket (#42022)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-14 05:14:25 -07:00
dependabot[bot]
d49365757c chore(deps-dev): bump @typescript-eslint/parser from 8.61.0 to 8.63.0 in /docs (#42025)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-14 05:14:18 -07:00
dependabot[bot]
68881a60ca chore(deps): bump caniuse-lite from 1.0.30001802 to 1.0.30001803 in /docs (#42026)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-14 05:14:14 -07:00
dependabot[bot]
41efdb3082 chore(deps-dev): bump webpack-sources from 3.5.0 to 3.5.1 in /superset-frontend (#42027)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-14 05:14:11 -07:00
dependabot[bot]
e86dcf7b82 chore(deps): bump actions/setup-java from 5.4.0 to 5.5.0 (#42028)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-14 05:14:08 -07:00
dependabot[bot]
35d05cc278 chore(deps-dev): bump @swc/plugin-emotion from 14.14.1 to 14.15.0 in /superset-frontend (#42029)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-14 05:14:04 -07:00
dependabot[bot]
115ce12ff7 chore(deps-dev): bump @typescript-eslint/parser from 8.62.1 to 8.63.0 in /superset-frontend (#42030)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-14 05:14:01 -07:00
dependabot[bot]
ccfccdf237 chore(deps-dev): bump oxlint from 1.72.0 to 1.73.0 in /superset-frontend (#42031)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-14 05:13:56 -07:00
Antonio Rivero
42523f8cc4 fix(async-queries): back off polling when async event requests keep failing (#42012) 2026-07-14 12:57:45 +02:00
Hans Yu
e165762bb7 refactor: begin transaction to automatically commit (#41979)
Co-authored-by: Evan Rusackas <evan@preset.io>
2026-07-13 22:45:40 -07:00
Hans Yu
8afad27c84 chore: convert warning to error if parameters not passed as collection (#41977) 2026-07-13 21:25:23 -07:00
Evan Rusackas
e420812eb7 test(filters): guard BIGINT filter value precision (#27510) (#41963)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-07-13 21:20:38 -07:00
Mike Bridge
993a43396a fix: restore all-files pre-commit checks (#42014)
Co-authored-by: Mike Bridge <michael.bridge@ext.preset.io>
Co-authored-by: Joe Li <joe@preset.io>
2026-07-13 17:35:55 -07:00
24 changed files with 1339 additions and 308 deletions

View File

@@ -37,7 +37,7 @@ jobs:
persist-credentials: false
submodules: recursive
- name: Setup Java
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
uses: actions/setup-java@0f481fcb613427c0f801b606911222b5b6f3083a # v5.5.0
with:
distribution: "temurin"
java-version: "11"

View File

@@ -23,7 +23,7 @@ jobs:
persist-credentials: false
submodules: recursive
- name: Setup Java
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
uses: actions/setup-java@0f481fcb613427c0f801b606911222b5b6f3083a # v5.5.0
with:
distribution: "temurin"
java-version: "11"

View File

@@ -71,7 +71,7 @@ jobs:
node-version-file: "./docs/.nvmrc"
- name: Setup Python
uses: ./.github/actions/setup-backend/
- uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
- uses: actions/setup-java@0f481fcb613427c0f801b606911222b5b6f3083a # v5.5.0
with:
distribution: "zulu"
java-version: "21"

View File

@@ -75,7 +75,7 @@ jobs:
- name: Use Node.js 20
# zizmor: ignore[cache-poisoning] - node only runs the supersetbot CLI; no dependency cache is enabled
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 20
package-manager-cache: false
@@ -133,7 +133,7 @@ jobs:
- name: Use Node.js 20
# zizmor: ignore[cache-poisoning] - node only runs the supersetbot CLI; no dependency cache is enabled
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 20
package-manager-cache: false

View File

@@ -63,7 +63,7 @@
"@swc/core": "^1.15.43",
"antd": "^6.5.0",
"baseline-browser-mapping": "^2.10.40",
"caniuse-lite": "^1.0.30001802",
"caniuse-lite": "^1.0.30001803",
"docusaurus-plugin-openapi-docs": "^5.1.0",
"docusaurus-theme-openapi-docs": "^5.1.0",
"js-yaml": "^5.2.0",
@@ -90,7 +90,7 @@
"@types/js-yaml": "^4.0.9",
"@types/react": "^19.1.8",
"@typescript-eslint/eslint-plugin": "^8.59.3",
"@typescript-eslint/parser": "^8.61.0",
"@typescript-eslint/parser": "^8.63.0",
"eslint": "^9.39.2",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.6",

View File

@@ -5518,7 +5518,7 @@
natural-compare "^1.4.0"
ts-api-utils "^2.5.0"
"@typescript-eslint/parser@8.62.1", "@typescript-eslint/parser@^8.61.0":
"@typescript-eslint/parser@8.62.1":
version "8.62.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.62.1.tgz#d3f7ba18f1bf78bfb7256fea021d1927b48e7080"
integrity sha512-sPhE4iHuJDSvoAiec+Ro8JyXw8f0ql13HFR82P99nCm9GwTEKG0KYLvDe6REk8BCXuit6vJAv/Yxg5ABaNS2rA==
@@ -5529,6 +5529,17 @@
"@typescript-eslint/visitor-keys" "8.62.1"
debug "^4.4.3"
"@typescript-eslint/parser@^8.63.0":
version "8.63.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.63.0.tgz#2993338c379903e6afc72c3532ae6e15b97334d4"
integrity sha512-gwh4gvvlaVDKKxyfxMG+Gnu1u9X0OQBwyGLkbwB65dIzBKnxeRiJlNFqlI3zwVhNXJIs6qV7mlFCn/BIajlVig==
dependencies:
"@typescript-eslint/scope-manager" "8.63.0"
"@typescript-eslint/types" "8.63.0"
"@typescript-eslint/typescript-estree" "8.63.0"
"@typescript-eslint/visitor-keys" "8.63.0"
debug "^4.4.3"
"@typescript-eslint/project-service@8.62.1":
version "8.62.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.62.1.tgz#78d880eb1cf6859b5ec263d04f95403e9f90ae47"
@@ -5538,6 +5549,15 @@
"@typescript-eslint/types" "^8.62.1"
debug "^4.4.3"
"@typescript-eslint/project-service@8.63.0":
version "8.63.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.63.0.tgz#01a3d0550a860127444a9939749ab434591cd71a"
integrity sha512-e5dh0/UI0ok53AlZ5wRkXCB32z/f2jUZqPR/ygAw5WYaSw8j9EoJWlS7wQjr/dmOaqWjnPIn2m+HhVPCMWGZVQ==
dependencies:
"@typescript-eslint/tsconfig-utils" "^8.63.0"
"@typescript-eslint/types" "^8.63.0"
debug "^4.4.3"
"@typescript-eslint/scope-manager@8.62.1":
version "8.62.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.62.1.tgz#7ee65e9a6eb3ccdc4816593a4ff38840306de88a"
@@ -5546,11 +5566,29 @@
"@typescript-eslint/types" "8.62.1"
"@typescript-eslint/visitor-keys" "8.62.1"
"@typescript-eslint/scope-manager@8.63.0":
version "8.63.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.63.0.tgz#c9cf7ecd234f7ec346f62e5c7d28dfaf4d507b4d"
integrity sha512-uUyfMWCnDSN8bCpcrY8nGP2BLkQ9Xn0GsipcONcpIDWhwhO4ZSyHvyS14U3X75mzxWxL3I2UZIrenTzdzcJO8A==
dependencies:
"@typescript-eslint/types" "8.63.0"
"@typescript-eslint/visitor-keys" "8.63.0"
"@typescript-eslint/tsconfig-utils@8.62.1", "@typescript-eslint/tsconfig-utils@^8.62.1":
version "8.62.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.62.1.tgz#e2b5f24fe721044189cb7e81117c96d75979d627"
integrity sha512-xadytJqX9vJVQ2fdQjkcIVigwaOJNWkpjdLt6cEQ+xPnrI1fkp+/jZE/I97k9KUjqtpd25i0HeyZf3T6dutv2g==
"@typescript-eslint/tsconfig-utils@8.63.0":
version "8.63.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.63.0.tgz#f7e1cf9a029bb71f4027ffa4194ba82afb564cd3"
integrity sha512-sUAbkulqBAsncKnbRP3+7CtQFRKicexnj7ZwNC6ddCR7EmrXvjvdCYMJbUIqMd6lwoEriZjwLo08aS5tSjVMHg==
"@typescript-eslint/tsconfig-utils@^8.63.0":
version "8.64.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.64.0.tgz#c62ac8ea9173c3cac8b38b8e66e30a046b548851"
integrity sha512-2yo8rRNKuzbVWQp5kslhANqZ2uDAeROQHBRZNPu8JDsHmeFNj/XJJhX/FhNUWmkHHvoNsKa6+tHJiig87EzsQw==
"@typescript-eslint/type-utils@8.62.1":
version "8.62.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.62.1.tgz#ebd30b13bacb13070917259a23309cf644121f9a"
@@ -5562,11 +5600,21 @@
debug "^4.4.3"
ts-api-utils "^2.5.0"
"@typescript-eslint/types@8.62.1", "@typescript-eslint/types@^8.62.1":
"@typescript-eslint/types@8.62.1":
version "8.62.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.62.1.tgz#c58be954e483b2fc98275374d5bcb40b99842dc1"
integrity sha512-ooCzJFaf+Hg+uG6fA3NRFGuFjlfNlDhBthbv4ZPU/0elCAFUfnyXUvf/WOpHz/jYwSmvU2GkR2LtyUfy1AxZ1Q==
"@typescript-eslint/types@8.63.0":
version "8.63.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.63.0.tgz#6b32b0a5913520554d81a986acfffba2d32b6963"
integrity sha512-xyLtl9DUBBFrcJS4x2pIqGLH68/tC2uOa4Z7pUteW09D3bXnnXUom4dyPikzWgB7llmIc1zoeI3aoUdC4rPK/Q==
"@typescript-eslint/types@^8.62.1", "@typescript-eslint/types@^8.63.0":
version "8.64.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.64.0.tgz#b41f8ef5dd40616908658b991197a9d486cda60b"
integrity sha512-qjhfuTfLXjA4IOzXvz0rTjT01BqEiIgPoUeMwiEjnaHKJMTNo8rH5pYW1a2L/0Dnux2fPC85AeyJoWaGa8WxTA==
"@typescript-eslint/typescript-estree@8.62.1":
version "8.62.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.62.1.tgz#98c1bb17635d5b026b24193a8d29188ac64380ff"
@@ -5582,6 +5630,21 @@
tinyglobby "^0.2.15"
ts-api-utils "^2.5.0"
"@typescript-eslint/typescript-estree@8.63.0":
version "8.63.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.63.0.tgz#a6f9c9cd290e98203ad29850b0d72529dc83be73"
integrity sha512-ygBkU+B7ex5UI/gKhaqexWev79uISfIv7XQCRNYO/jmD8rGLPyWLAb3KMRT6nd8Gt9bmUBi9+iX6tBdYfOY81Q==
dependencies:
"@typescript-eslint/project-service" "8.63.0"
"@typescript-eslint/tsconfig-utils" "8.63.0"
"@typescript-eslint/types" "8.63.0"
"@typescript-eslint/visitor-keys" "8.63.0"
debug "^4.4.3"
minimatch "^10.2.2"
semver "^7.7.3"
tinyglobby "^0.2.15"
ts-api-utils "^2.5.0"
"@typescript-eslint/utils@8.62.1":
version "8.62.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.62.1.tgz#1622b75c7e6df308181dd0b44855dc4228da0457"
@@ -5600,6 +5663,14 @@
"@typescript-eslint/types" "8.62.1"
eslint-visitor-keys "^5.0.0"
"@typescript-eslint/visitor-keys@8.63.0":
version "8.63.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.63.0.tgz#2853a8e33b52f23570338f96cc89cb223daabd44"
integrity sha512-UexrHGnGTpbuQHct2ExOc2ZcFbGUS9FOesCxxqdBGcpI1BxYu/LZ6U8Aq6/72XtF/qRBk9nhuGHFJIXXMhPMdw==
dependencies:
"@typescript-eslint/types" "8.63.0"
eslint-visitor-keys "^5.0.0"
"@ungap/structured-clone@^1.0.0":
version "1.3.2"
resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.3.2.tgz#a03ad82cd5676414d068ba86f880c5681194aadf"
@@ -6567,10 +6638,10 @@ caniuse-api@^3.0.0:
lodash.memoize "^4.1.2"
lodash.uniq "^4.5.0"
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001799, caniuse-lite@^1.0.30001802:
version "1.0.30001802"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001802.tgz#2671fb13d468930586c56ffa80feb1c51e18ec69"
integrity sha512-vmv8ub2xwTNmljSKf82mtCk5JH7hC+YgzLj3P5zotvA0tPQ9016tdNNOG8WRca1IxOnhSsivB+J0z5FeE5LOUw==
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001799, caniuse-lite@^1.0.30001803:
version "1.0.30001803"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001803.tgz#b2a5d696e042bc8304dcd4942c39fe330fbbcb24"
integrity sha512-g/uHREV2ZpK9qMalCsWaxmA6ol+DX8GYhuf3T40RKoP+oL7vhRJh8LNt73PCjpnR6l14FzfPrB5Yux4PKm2meg==
ccount@^2.0.0:
version "2.0.1"

View File

@@ -37,6 +37,8 @@ filterwarnings =
# error:"TaggedObject" object is being merged into a Session:sqlalchemy.exc.RemovedIn20Warning
# error:The autoload parameter is deprecated:sqlalchemy.exc.RemovedIn20Warning
# error:The connection.execute\(\) method:sqlalchemy.exc.RemovedIn20Warning
error:The current statement is being autocommitted using implicit autocommit:sqlalchemy.exc.RemovedIn20Warning
error:The connection.execute\(\) method:sqlalchemy.exc.RemovedIn20Warning
# error:The current statement is being autocommitted using implicit autocommit:sqlalchemy.exc.RemovedIn20Warning
# error:The ``declarative_base\(\)`` function is now available:sqlalchemy.exc.RemovedIn20Warning
error:The Engine.execute\(\) method is considered legacy:sqlalchemy.exc.RemovedIn20Warning

View File

@@ -188,7 +188,7 @@
"@storybook/test-runner": "0.24.4",
"@svgr/webpack": "^8.1.0",
"@swc/core": "^1.15.43",
"@swc/plugin-emotion": "^14.14.1",
"@swc/plugin-emotion": "^14.15.0",
"@swc/plugin-transform-imports": "^12.5.0",
"@testing-library/dom": "^9.3.4",
"@testing-library/jest-dom": "^6.9.1",
@@ -216,7 +216,7 @@
"@types/tinycolor2": "^1.4.3",
"@types/unzipper": "^0.10.11",
"@typescript-eslint/eslint-plugin": "^8.62.1",
"@typescript-eslint/parser": "^8.62.1",
"@typescript-eslint/parser": "^8.63.0",
"babel-jest": "^30.4.1",
"babel-loader": "^10.1.1",
"babel-plugin-dynamic-import-node": "^2.3.3",
@@ -260,7 +260,7 @@
"mini-css-extract-plugin": "^2.10.2",
"minimizer-webpack-plugin": "^5.6.1",
"open-cli": "^9.0.0",
"oxlint": "^1.72.0",
"oxlint": "^1.73.0",
"po2json": "^0.4.5",
"postcss-styled-syntax": "^0.7.2",
"prettier": "3.9.4",
@@ -289,7 +289,7 @@
"webpack-cli": "^7.0.3",
"webpack-dev-server": "^5.2.5",
"webpack-manifest-plugin": "^6.0.1",
"webpack-sources": "^3.5.0",
"webpack-sources": "^3.5.1",
"webpack-visualizer-plugin2": "^2.0.0"
},
"engines": {
@@ -8443,9 +8443,9 @@
]
},
"node_modules/@oxlint/binding-android-arm-eabi": {
"version": "1.72.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.72.0.tgz",
"integrity": "sha512-zhCmvn+1Mj3UchAc/90i99S0t7jJUsHmFVSPg4UWrjO8b8eaSGwscgO6QAUtvHBstkjQwBttQNswEnAF1mIQdA==",
"version": "1.73.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.73.0.tgz",
"integrity": "sha512-HZQRN/UMBu+Ut+/9MiAChkbP4qZqrNOWBcNI45vOT40GVhbGR0JgHB87L48D4iAqFQIdVmeQYtV9RF89AjTKkg==",
"cpu": [
"arm"
],
@@ -8460,9 +8460,9 @@
}
},
"node_modules/@oxlint/binding-android-arm64": {
"version": "1.72.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.72.0.tgz",
"integrity": "sha512-mtH+aY/ozv1eZoCUC2owjFAtyNBKHpJHygKeEu9zXXnQGW1Q2/qOpvx+I+Lf23+TvTz66F4iiXUbl2cGvoLPCQ==",
"version": "1.73.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.73.0.tgz",
"integrity": "sha512-Gp+KJRylv2aW7thRpG5p1KTxZq4ZJFbWowrKzufNq9d3ssl3r3JviYV45/+p+7CN1Nv0zDd1e8Ex0b/HUDq4TQ==",
"cpu": [
"arm64"
],
@@ -8477,9 +8477,9 @@
}
},
"node_modules/@oxlint/binding-darwin-arm64": {
"version": "1.72.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.72.0.tgz",
"integrity": "sha512-EvnajNPDtfknB3ZieeOOyDTwJn9QXDiwfnF4ZDQqART6RG6hjY4WigQcZdGoK2dkB3e1vrmEzN9aYbQCUkh/gQ==",
"version": "1.73.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.73.0.tgz",
"integrity": "sha512-3de96NdtXhxERMjIz7wsp2HYMY6pMQycGxFWac2mFecAx6VeARF/IqFb1QIaqiCRIdfzBwzTed+pCTCoiS+CYA==",
"cpu": [
"arm64"
],
@@ -8494,9 +8494,9 @@
}
},
"node_modules/@oxlint/binding-darwin-x64": {
"version": "1.72.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.72.0.tgz",
"integrity": "sha512-ZkCdEa/G80A7vEHfeCDz/+L3m33DE73v32mDKhgOIgz8Uwf0DFcK7+uu6qC+7LEhmz5fpOe1osWKyjSNMydFIQ==",
"version": "1.73.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.73.0.tgz",
"integrity": "sha512-5zx/uPW32TiaOeVY1dQ/H5iOf0K1HOdFKOJhLqGl4o63+i1fpzoqqu/mKtd7OFgFjNCdhlyTGgjVkQTZm1ELcg==",
"cpu": [
"x64"
],
@@ -8511,9 +8511,9 @@
}
},
"node_modules/@oxlint/binding-freebsd-x64": {
"version": "1.72.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.72.0.tgz",
"integrity": "sha512-NroXv2vh+sxVY1uya/rM5pjhx1hm8BzlYpx9q67QP0Xhw5MH2bf5GJylpvLEC+781p1Xli/317EoV9AlGwViag==",
"version": "1.73.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.73.0.tgz",
"integrity": "sha512-qNe4gKHaGnLuZJ8toUg90JAa0S2vTVvDw+0bRi3q1avXZXDT4u5mMeECf3nD4HYrbdn1O7dXqWut4onY/yx/Xg==",
"cpu": [
"x64"
],
@@ -8528,9 +8528,9 @@
}
},
"node_modules/@oxlint/binding-linux-arm-gnueabihf": {
"version": "1.72.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.72.0.tgz",
"integrity": "sha512-0NDywYgfj279Ou/BcQuCYSj7NJwBfmWn5qc5uGO/Ny7fUWmXyIpvawqX/8acQlWG6IXelJsJhj+JAy6sjsKj0A==",
"version": "1.73.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.73.0.tgz",
"integrity": "sha512-cCehYh5hTbfShm/fxTD6wwrGUWIpvX+N5OxmAMhFhDeTGXvw+BeNj889tpxsFQ9ZLatQ6wImuY8tsKLZ+FMz7w==",
"cpu": [
"arm"
],
@@ -8545,9 +8545,9 @@
}
},
"node_modules/@oxlint/binding-linux-arm-musleabihf": {
"version": "1.72.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.72.0.tgz",
"integrity": "sha512-4vpXB06h65Ezsy4hRyrGjGrfa1SkVPii09yaajiYhmVpgsFiLD+KNxIx/BNAY+XiO+i1yqp9HHdwqM8VTqa5XQ==",
"version": "1.73.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.73.0.tgz",
"integrity": "sha512-d5j5GDU/2dMgjVhw7TQT9ITrsIr1Y02KEXKyVGIXUkD+KiaxE9TP65FS2ZdgTBemQvoRL+gSBdbrIm3cQIeacg==",
"cpu": [
"arm"
],
@@ -8562,13 +8562,16 @@
}
},
"node_modules/@oxlint/binding-linux-arm64-gnu": {
"version": "1.72.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.72.0.tgz",
"integrity": "sha512-immaN4g2ZGFiOkKrvRX9LvzZdd2GkQM5wR+UyzYyUuyhUTXGQ4HKUJH18xp4G8OfhCVaVAJfKZxwE1r8+4hhaQ==",
"version": "1.73.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.73.0.tgz",
"integrity": "sha512-Eyf1SrP3+yR1DI3OJgOY2Pvrr9dWP9TK37xPaDYycwTtlGlI45erJAVIfH5/m/xosDt6BupJYEFi47bvbTuuyw==",
"cpu": [
"arm64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -8579,13 +8582,16 @@
}
},
"node_modules/@oxlint/binding-linux-arm64-musl": {
"version": "1.72.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.72.0.tgz",
"integrity": "sha512-JGHS9Mnr7iWyyLDxgCv1MhzVpAckgptg00F2gnxt/GD7lQ2SW1BRcxHqhSTaSdDpjWRrBkBxMMh4+Hn3aVtExg==",
"version": "1.73.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.73.0.tgz",
"integrity": "sha512-IlT/OJApEDKaMmCooHuncgJZbbCe7T5QIWmTZBEtYscWvzPQuuEinVcid6kwQRVQOUdb7PUCz4jQHnaYXdfJXw==",
"cpu": [
"arm64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -8596,13 +8602,16 @@
}
},
"node_modules/@oxlint/binding-linux-ppc64-gnu": {
"version": "1.72.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.72.0.tgz",
"integrity": "sha512-AOYgBZqxNshrg83P9v0RYv+m8s10Cqkj4/PxXFDhcS3k7FqsIG5+CxErshZCIN7G8iy4Y+VGfAsuEdar8AcbBg==",
"version": "1.73.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.73.0.tgz",
"integrity": "sha512-L+JYcb/vdg5fmcH08V6o0YYLU28cTH1SPNulwJdvK9NK49aXSkYy6oNpKBmddArVOXYqNepriDGiZ04G54kh1Q==",
"cpu": [
"ppc64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -8613,13 +8622,16 @@
}
},
"node_modules/@oxlint/binding-linux-riscv64-gnu": {
"version": "1.72.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.72.0.tgz",
"integrity": "sha512-QMybPS5ij3/vrKG67mqzHwW++91sYxK/PPUVi6SBtNCEzW4niS52fVBdXbQ6nou0wWbUPEpx8Sl/ZjtgE3clXA==",
"version": "1.73.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.73.0.tgz",
"integrity": "sha512-Qtk0g3bKV6OwWjIm7R8kQN1uOZRKQt/MODK2a8QfkwhTpXBD53ozx5XLVWLGDQAVyp2otLW4D2wB98XfAfMPGA==",
"cpu": [
"riscv64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -8630,13 +8642,16 @@
}
},
"node_modules/@oxlint/binding-linux-riscv64-musl": {
"version": "1.72.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.72.0.tgz",
"integrity": "sha512-gOc3W7JV0PXRpIL7stUlLe3Wa9Gp0Kdlup87IT3gHDvPKck2xNgMIl/Gs2lldYY2lyXZDC4rWi3hmoLUobkgbQ==",
"version": "1.73.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.73.0.tgz",
"integrity": "sha512-wX0NQKZVxltkAOVmzFcpOaMpdaUvsq1Eqpx9tkAfl71UdkTlSo1R4AdAnGccR1Fm2+TzFgZ22CyyGuZ41RDr/A==",
"cpu": [
"riscv64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -8647,13 +8662,16 @@
}
},
"node_modules/@oxlint/binding-linux-s390x-gnu": {
"version": "1.72.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.72.0.tgz",
"integrity": "sha512-rpGxph+FjjHcYI5q6uxB3Az+tnfmEnDbSA8+PK9ZE/VzyUAkvBOMeuY7ZQMhu5mpZH7YQDsTdW6Cx4kV/msc6w==",
"version": "1.73.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.73.0.tgz",
"integrity": "sha512-vPe7UGBMWyiLTtnqS4xxgMQFSFGmtQwhwCxuiw6lXygaO6bVt0D8dFVg8Xv05eaiN3ybC0HXXHUAohFMFvqoCQ==",
"cpu": [
"s390x"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -8664,13 +8682,16 @@
}
},
"node_modules/@oxlint/binding-linux-x64-gnu": {
"version": "1.72.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.72.0.tgz",
"integrity": "sha512-WND+uhf/Ko13SLqQMWQUgsZuLvYYEvL0ZKgg0tgGYfLqxG7l8Ju123fHDMJyYSDl5E3bUbpFUuii/OvMreFQzw==",
"version": "1.73.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.73.0.tgz",
"integrity": "sha512-2CwIWr9cemFC/CbRBWZvuk5mffz6ObmfFkfcC/9rTQ7f+icNhYr2kOjf9Rt8lLvugvkdGDOmkoVoFFHh6ClCTw==",
"cpu": [
"x64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -8681,13 +8702,16 @@
}
},
"node_modules/@oxlint/binding-linux-x64-musl": {
"version": "1.72.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.72.0.tgz",
"integrity": "sha512-SrpbrUL70nG9vh6zP4/oKHWgLuHquwsr7MW9XOn0olBVgh10Uqr8qscKhQoBGEn6olK/IUpn5GSKcdQ5AjUhGA==",
"version": "1.73.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.73.0.tgz",
"integrity": "sha512-nDadfJgg7NBBxG0N560wOe7LLX5QiYp6qBaI7viuk5EUORFBktU/NfV0MbTqU3gTqQDCh4VyxKdo5VADxk9w8Q==",
"cpu": [
"x64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -8698,9 +8722,9 @@
}
},
"node_modules/@oxlint/binding-openharmony-arm64": {
"version": "1.72.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.72.0.tgz",
"integrity": "sha512-qkrsEn6NmgFKr7U/QnezQMb+q/vzAy0Dd9Y95gQGQTyjzDLN+HRZMuM5u70iyH4nBLCfKBzhjMsYCehKay2jyg==",
"version": "1.73.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.73.0.tgz",
"integrity": "sha512-wGjJC+NLH9xP+IKGn9RDW94ojJR/wPbg5WCnQjj/oReaOtCQthr8ws1zICe77JFmo4ouUdeTHHZL/ESGiF6Pmw==",
"cpu": [
"arm64"
],
@@ -8715,9 +8739,9 @@
}
},
"node_modules/@oxlint/binding-win32-arm64-msvc": {
"version": "1.72.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.72.0.tgz",
"integrity": "sha512-LWR6ZlFZph+KPjXv8opgZsXRDCdrdQe8VL8Cg9zxCoBS73h6znzZpydVgmdnwj8mB9AuSM5jxEgDJDpQkjboeg==",
"version": "1.73.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.73.0.tgz",
"integrity": "sha512-I7X47GPGljw225YUQ5SbC/rb1Kkdrd0yQf0x+hYxeKS6DpfjMbo9ccQPQ6LNY6BoJQ1sHhgDUGuMn5Vg5gHT6w==",
"cpu": [
"arm64"
],
@@ -8732,9 +8756,9 @@
}
},
"node_modules/@oxlint/binding-win32-ia32-msvc": {
"version": "1.72.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.72.0.tgz",
"integrity": "sha512-yt6HEh7IsHvtjRWtmeZRX134eaXKHq5Gnqlf1xBJdJl1JtdoRUEJw3nAxpZoUDS860cX/foKbztO441anVBtVQ==",
"version": "1.73.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.73.0.tgz",
"integrity": "sha512-5lWj+3h+74Fm1jYOO9qkJA4xkAlZA099DkXppuXsk7UpnpZLttsefrZU469vChGaG6hcSqrkKXQOvMTZtbjeNg==",
"cpu": [
"ia32"
],
@@ -8749,9 +8773,9 @@
}
},
"node_modules/@oxlint/binding-win32-x64-msvc": {
"version": "1.72.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.72.0.tgz",
"integrity": "sha512-b2eKFD2hX7tIwmo/cyH6TDq8vzWRZ2qNHrzoGntUTmq0h3zQh/uX3eTSHCwI8OB/ADQfJCRelLItK8BsxuucDA==",
"version": "1.73.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.73.0.tgz",
"integrity": "sha512-WaNRvh4f6zY9CvUQk2YoA1O90ieWrIklI84+HXFr9Isjz9CSESrdqo/RtIYt4Dll/cAchqGDMehfaZd0vqEFZw==",
"cpu": [
"x64"
],
@@ -11326,9 +11350,9 @@
}
},
"node_modules/@swc/plugin-emotion": {
"version": "14.14.1",
"resolved": "https://registry.npmjs.org/@swc/plugin-emotion/-/plugin-emotion-14.14.1.tgz",
"integrity": "sha512-2EgdUStcBO6Zwpept8JVEKmP4AInL6UoYbcWq9G5blc5MSjRW4bn3KUTVEBrxCAklGq/79YBjsV/sqKqgqg4zA==",
"version": "14.15.0",
"resolved": "https://registry.npmjs.org/@swc/plugin-emotion/-/plugin-emotion-14.15.0.tgz",
"integrity": "sha512-nCsTO7mOOPz2UnT3N6YWb014uI0CVxeKg53A/KM/CvuSIE6H3KPkhaziJQ3q2jI3u3LfFuDKEnU5ZmB1330Dqg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
@@ -12741,16 +12765,16 @@
}
},
"node_modules/@typescript-eslint/parser": {
"version": "8.62.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.62.1.tgz",
"integrity": "sha512-sPhE4iHuJDSvoAiec+Ro8JyXw8f0ql13HFR82P99nCm9GwTEKG0KYLvDe6REk8BCXuit6vJAv/Yxg5ABaNS2rA==",
"version": "8.63.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.63.0.tgz",
"integrity": "sha512-gwh4gvvlaVDKKxyfxMG+Gnu1u9X0OQBwyGLkbwB65dIzBKnxeRiJlNFqlI3zwVhNXJIs6qV7mlFCn/BIajlVig==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/scope-manager": "8.62.1",
"@typescript-eslint/types": "8.62.1",
"@typescript-eslint/typescript-estree": "8.62.1",
"@typescript-eslint/visitor-keys": "8.62.1",
"@typescript-eslint/scope-manager": "8.63.0",
"@typescript-eslint/types": "8.63.0",
"@typescript-eslint/typescript-estree": "8.63.0",
"@typescript-eslint/visitor-keys": "8.63.0",
"debug": "^4.4.3"
},
"engines": {
@@ -12765,6 +12789,175 @@
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/project-service": {
"version": "8.63.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.63.0.tgz",
"integrity": "sha512-e5dh0/UI0ok53AlZ5wRkXCB32z/f2jUZqPR/ygAw5WYaSw8j9EoJWlS7wQjr/dmOaqWjnPIn2m+HhVPCMWGZVQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.63.0",
"@typescript-eslint/types": "^8.63.0",
"debug": "^4.4.3"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/scope-manager": {
"version": "8.63.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.63.0.tgz",
"integrity": "sha512-uUyfMWCnDSN8bCpcrY8nGP2BLkQ9Xn0GsipcONcpIDWhwhO4ZSyHvyS14U3X75mzxWxL3I2UZIrenTzdzcJO8A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.63.0",
"@typescript-eslint/visitor-keys": "8.63.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.63.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.63.0.tgz",
"integrity": "sha512-sUAbkulqBAsncKnbRP3+7CtQFRKicexnj7ZwNC6ddCR7EmrXvjvdCYMJbUIqMd6lwoEriZjwLo08aS5tSjVMHg==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/types": {
"version": "8.63.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.63.0.tgz",
"integrity": "sha512-xyLtl9DUBBFrcJS4x2pIqGLH68/tC2uOa4Z7pUteW09D3bXnnXUom4dyPikzWgB7llmIc1zoeI3aoUdC4rPK/Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": {
"version": "8.63.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.63.0.tgz",
"integrity": "sha512-ygBkU+B7ex5UI/gKhaqexWev79uISfIv7XQCRNYO/jmD8rGLPyWLAb3KMRT6nd8Gt9bmUBi9+iX6tBdYfOY81Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/project-service": "8.63.0",
"@typescript-eslint/tsconfig-utils": "8.63.0",
"@typescript-eslint/types": "8.63.0",
"@typescript-eslint/visitor-keys": "8.63.0",
"debug": "^4.4.3",
"minimatch": "^10.2.2",
"semver": "^7.7.3",
"tinyglobby": "^0.2.15",
"ts-api-utils": "^2.5.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/visitor-keys": {
"version": "8.63.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.63.0.tgz",
"integrity": "sha512-UexrHGnGTpbuQHct2ExOc2ZcFbGUS9FOesCxxqdBGcpI1BxYu/LZ6U8Aq6/72XtF/qRBk9nhuGHFJIXXMhPMdw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.63.0",
"eslint-visitor-keys": "^5.0.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/parser/node_modules/balanced-match": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
"dev": true,
"license": "MIT",
"engines": {
"node": "18 || 20 || >=22"
}
},
"node_modules/@typescript-eslint/parser/node_modules/brace-expansion": {
"version": "5.0.7",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz",
"integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^4.0.2"
},
"engines": {
"node": "18 || 20 || >=22"
}
},
"node_modules/@typescript-eslint/parser/node_modules/eslint-visitor-keys": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
"integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": "^20.19.0 || ^22.13.0 || >=24"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/@typescript-eslint/parser/node_modules/minimatch": {
"version": "10.2.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
"integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
"brace-expansion": "^5.0.5"
},
"engines": {
"node": "18 || 20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/@typescript-eslint/project-service": {
"version": "8.62.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.62.1.tgz",
@@ -32150,9 +32343,9 @@
}
},
"node_modules/oxlint": {
"version": "1.72.0",
"resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.72.0.tgz",
"integrity": "sha512-1rhdZIP/EvoI91ABIwNU5Q8+bWf8mjrS5UzIOZld4d4bXxJvtlUhlQvaoTogIGin/qdErMOrwaIJvCSIAKTLhA==",
"version": "1.73.0",
"resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.73.0.tgz",
"integrity": "sha512-u91G9TJzU6yqKWNZUYprQB07W7YvntZXaRxQ6CkoytepYhLWUXWsr1M8zUJ34VatNPuUAr3Z8GH+O2A331CluQ==",
"dev": true,
"license": "MIT",
"bin": {
@@ -32165,28 +32358,28 @@
"url": "https://github.com/sponsors/Boshen"
},
"optionalDependencies": {
"@oxlint/binding-android-arm-eabi": "1.72.0",
"@oxlint/binding-android-arm64": "1.72.0",
"@oxlint/binding-darwin-arm64": "1.72.0",
"@oxlint/binding-darwin-x64": "1.72.0",
"@oxlint/binding-freebsd-x64": "1.72.0",
"@oxlint/binding-linux-arm-gnueabihf": "1.72.0",
"@oxlint/binding-linux-arm-musleabihf": "1.72.0",
"@oxlint/binding-linux-arm64-gnu": "1.72.0",
"@oxlint/binding-linux-arm64-musl": "1.72.0",
"@oxlint/binding-linux-ppc64-gnu": "1.72.0",
"@oxlint/binding-linux-riscv64-gnu": "1.72.0",
"@oxlint/binding-linux-riscv64-musl": "1.72.0",
"@oxlint/binding-linux-s390x-gnu": "1.72.0",
"@oxlint/binding-linux-x64-gnu": "1.72.0",
"@oxlint/binding-linux-x64-musl": "1.72.0",
"@oxlint/binding-openharmony-arm64": "1.72.0",
"@oxlint/binding-win32-arm64-msvc": "1.72.0",
"@oxlint/binding-win32-ia32-msvc": "1.72.0",
"@oxlint/binding-win32-x64-msvc": "1.72.0"
"@oxlint/binding-android-arm-eabi": "1.73.0",
"@oxlint/binding-android-arm64": "1.73.0",
"@oxlint/binding-darwin-arm64": "1.73.0",
"@oxlint/binding-darwin-x64": "1.73.0",
"@oxlint/binding-freebsd-x64": "1.73.0",
"@oxlint/binding-linux-arm-gnueabihf": "1.73.0",
"@oxlint/binding-linux-arm-musleabihf": "1.73.0",
"@oxlint/binding-linux-arm64-gnu": "1.73.0",
"@oxlint/binding-linux-arm64-musl": "1.73.0",
"@oxlint/binding-linux-ppc64-gnu": "1.73.0",
"@oxlint/binding-linux-riscv64-gnu": "1.73.0",
"@oxlint/binding-linux-riscv64-musl": "1.73.0",
"@oxlint/binding-linux-s390x-gnu": "1.73.0",
"@oxlint/binding-linux-x64-gnu": "1.73.0",
"@oxlint/binding-linux-x64-musl": "1.73.0",
"@oxlint/binding-openharmony-arm64": "1.73.0",
"@oxlint/binding-win32-arm64-msvc": "1.73.0",
"@oxlint/binding-win32-ia32-msvc": "1.73.0",
"@oxlint/binding-win32-x64-msvc": "1.73.0"
},
"peerDependencies": {
"oxlint-tsgolint": ">=0.22.1",
"oxlint-tsgolint": ">=0.24.0",
"vite-plus": "*"
},
"peerDependenciesMeta": {
@@ -42210,9 +42403,9 @@
}
},
"node_modules/webpack-sources": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.5.0.tgz",
"integrity": "sha512-HPuy+uuoTCaaoEoI1LQ3JN9+vrPBvEesnnX1jADHy728cHSMlq4wUc4afYqahq2B1mhQVZxCXOkNTnXltr+2vQ==",
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.5.1.tgz",
"integrity": "sha512-jyuiGJdtvY434z5bUZrjz67v76/ePNvFZTp9Mdz29IlH4+GPsgyGjiv0fKI+M7BdkU6ADjulUcKAd3tUK3WlEw==",
"dev": true,
"license": "MIT",
"engines": {

View File

@@ -273,7 +273,7 @@
"@storybook/test-runner": "0.24.4",
"@svgr/webpack": "^8.1.0",
"@swc/core": "^1.15.43",
"@swc/plugin-emotion": "^14.14.1",
"@swc/plugin-emotion": "^14.15.0",
"@swc/plugin-transform-imports": "^12.5.0",
"@testing-library/dom": "^9.3.4",
"@testing-library/jest-dom": "^6.9.1",
@@ -301,7 +301,7 @@
"@types/tinycolor2": "^1.4.3",
"@types/unzipper": "^0.10.11",
"@typescript-eslint/eslint-plugin": "^8.62.1",
"@typescript-eslint/parser": "^8.62.1",
"@typescript-eslint/parser": "^8.63.0",
"babel-jest": "^30.4.1",
"babel-loader": "^10.1.1",
"babel-plugin-dynamic-import-node": "^2.3.3",
@@ -345,7 +345,7 @@
"mini-css-extract-plugin": "^2.10.2",
"minimizer-webpack-plugin": "^5.6.1",
"open-cli": "^9.0.0",
"oxlint": "^1.72.0",
"oxlint": "^1.73.0",
"po2json": "^0.4.5",
"postcss-styled-syntax": "^0.7.2",
"prettier": "3.9.4",
@@ -374,7 +374,7 @@
"webpack-cli": "^7.0.3",
"webpack-dev-server": "^5.2.5",
"webpack-manifest-plugin": "^6.0.1",
"webpack-sources": "^3.5.0",
"webpack-sources": "^3.5.1",
"webpack-visualizer-plugin2": "^2.0.0"
},
"peerDependencies": {

View File

@@ -37,6 +37,7 @@ export class DashboardPage {
FILTER_BAR_SETTINGS: '[data-test="filterbar-orientation-icon"]',
APPLY_FILTERS_BUTTON:
'[data-test="filter-bar__apply-button"], [data-test="filterbar-action-buttons"] button[type="submit"]',
DASHBOARD_TABS: '[data-test="dashboard-component-tabs"]',
} as const;
constructor(page: Page) {
@@ -108,6 +109,34 @@ export class DashboardPage {
});
}
/**
* Locator for the individual tabs of the top-level tab bar.
* A dashboard can contain several nested tab bars; the top-level one is the
* first `dashboard-component-tabs` rendered in the DOM. The nav list is
* scoped to the immediate child (`:scope >`) so that tabs belonging to nested
* tab bars rendered inside this container's content are not counted.
*/
topLevelTabs(): Locator {
return this.page
.locator(DashboardPage.SELECTORS.DASHBOARD_TABS)
.first()
.locator(
':scope > [data-test="nav-list"] .ant-tabs-nav-list > .ant-tabs-tab',
);
}
/**
* Switch to the nth top-level tab (0-indexed) and wait for it to become active.
*/
async switchToTopLevelTab(index: number): Promise<void> {
const tab = this.topLevelTabs().nth(index);
await tab.click();
await tab.and(this.page.locator('.ant-tabs-tab-active')).waitFor({
state: 'attached',
timeout: TIMEOUT.API_RESPONSE,
});
}
/**
* Gets a Display Control heading by name.
* @param name - The Display Control name

View File

@@ -0,0 +1,276 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* E2E migration of the Cypress "Dashboard tabs" suite (dashboard/tabs.test.ts).
*
* Only one of the three original cases is a genuine end-to-end behaviour:
* "should update size when switch tab". A chart living in an inactive (hidden)
* tab must re-measure and refit its container when the tab is revealed after the
* available width has changed — a real browser layout-reflow that can only be
* exercised against a rendered chart in a real dashboard. The other two cases
* ("should switch tabs" asserted only the `ant-tabs-tab-active` CSS class, and
* "should send new queries when tab becomes visible" was already skipped) are
* DOM/state assertions with no backend invariant and belong in component/RTL
* coverage, so they are intentionally not migrated here.
*
* The original relied on a seeded tabbed dashboard and expanded the native
* filter bar to change the available width. This migration builds the dashboard
* hermetically (two top-level tabs, a width-sensitive treemap in the first) and
* shrinks the viewport while the treemap is hidden — the equivalent width change,
* with no dependency on seeded data.
*
* CI green => the treemap reflowed to the narrower width and fit its container
* (no horizontal overflow) after the tab switch.
* CI red => the chart kept a stale size and overflowed its container, or the
* width never changed (the reflow was never exercised).
*/
import { testWithAssets, expect } from '../../helpers/fixtures';
import { apiPost, apiPut } from '../../helpers/api/requests';
import { apiPostDashboard } from '../../helpers/api/dashboard';
import { getDatasetByName } from '../../helpers/api/dataset';
import { TIMEOUT } from '../../utils/constants';
import { DashboardPage } from '../../pages/DashboardPage';
const DATASET_NAME = 'birth_names';
const WIDE_VIEWPORT = { width: 1400, height: 900 };
const NARROW_VIEWPORT = { width: 700, height: 900 };
testWithAssets(
'chart in a hidden tab refits its container after the tab is revealed at a new width',
async ({ page, testAssets }) => {
testWithAssets.setTimeout(TIMEOUT.SLOW_TEST);
const dataset = await getDatasetByName(page, DATASET_NAME);
if (!dataset) {
throw new Error(`Dataset ${DATASET_NAME} not found`);
}
const datasetId = dataset.id;
const datasource = `${datasetId}__table`;
// Tab A holds a width-sensitive treemap (echarts sizes it to fill the
// container); Tab B holds a table so the second tab has real content.
const chartSpecs = [
{
slug: 'treemap',
params: {
datasource,
viz_type: 'treemap_v2',
metric: 'count',
groupby: ['gender'],
row_limit: 100,
},
},
{
slug: 'table',
params: {
datasource,
viz_type: 'table',
query_mode: 'aggregate',
groupby: ['name'],
metrics: ['count'],
row_limit: 100,
},
},
];
const chartIds: Record<string, number> = {};
for (const { slug, params } of chartSpecs) {
const resp = await apiPost(page, 'api/v1/chart/', {
slice_name: `tabs_${slug}_${Date.now()}`,
viz_type: params.viz_type,
datasource_id: datasetId,
datasource_type: 'table',
params: JSON.stringify(params),
});
expect(resp.ok()).toBe(true);
const body = await resp.json();
const chartId: number = body.id ?? body.result?.id;
testAssets.trackChart(chartId);
chartIds[slug] = chartId;
}
const treemapKey = `CHART-${chartIds.treemap}`;
const tableKey = `CHART-${chartIds.table}`;
// Top-level tabs live inside GRID_ID: ROOT -> GRID -> TABS -> TAB -> ROW -> CHART.
const positionJson: Record<string, unknown> = {
DASHBOARD_VERSION_KEY: 'v2',
ROOT_ID: { type: 'ROOT', id: 'ROOT_ID', children: ['GRID_ID'] },
GRID_ID: {
type: 'GRID',
id: 'GRID_ID',
children: ['TABS-TOP'],
parents: ['ROOT_ID'],
},
'TABS-TOP': {
type: 'TABS',
id: 'TABS-TOP',
children: ['TAB-A', 'TAB-B'],
parents: ['ROOT_ID', 'GRID_ID'],
meta: {},
},
'TAB-A': {
type: 'TAB',
id: 'TAB-A',
children: ['ROW-A'],
parents: ['ROOT_ID', 'GRID_ID', 'TABS-TOP'],
meta: {
text: 'Tab A',
defaultText: 'Tab title',
placeholder: 'Tab title',
},
},
'TAB-B': {
type: 'TAB',
id: 'TAB-B',
children: ['ROW-B'],
parents: ['ROOT_ID', 'GRID_ID', 'TABS-TOP'],
meta: {
text: 'Tab B',
defaultText: 'Tab title',
placeholder: 'Tab title',
},
},
'ROW-A': {
type: 'ROW',
id: 'ROW-A',
children: [treemapKey],
parents: ['ROOT_ID', 'GRID_ID', 'TABS-TOP', 'TAB-A'],
meta: { background: 'BACKGROUND_TRANSPARENT' },
},
'ROW-B': {
type: 'ROW',
id: 'ROW-B',
children: [tableKey],
parents: ['ROOT_ID', 'GRID_ID', 'TABS-TOP', 'TAB-B'],
meta: { background: 'BACKGROUND_TRANSPARENT' },
},
[treemapKey]: {
type: 'CHART',
id: treemapKey,
children: [],
parents: ['ROOT_ID', 'GRID_ID', 'TABS-TOP', 'TAB-A', 'ROW-A'],
meta: {
chartId: chartIds.treemap,
width: 12,
height: 50,
sliceName: 'treemap',
},
},
[tableKey]: {
type: 'CHART',
id: tableKey,
children: [],
parents: ['ROOT_ID', 'GRID_ID', 'TABS-TOP', 'TAB-B', 'ROW-B'],
meta: {
chartId: chartIds.table,
width: 12,
height: 50,
sliceName: 'table',
},
},
};
const dashResp = await apiPostDashboard(page, {
dashboard_title: `tabs_resize_${Date.now()}`,
published: true,
position_json: JSON.stringify(positionJson),
});
expect(dashResp.ok()).toBe(true);
const dashBody = await dashResp.json();
const dashboardId: number = dashBody.result?.id ?? dashBody.id;
testAssets.trackDashboard(dashboardId);
for (const chartId of Object.values(chartIds)) {
await apiPut(page, `api/v1/chart/${chartId}`, {
dashboards: [dashboardId],
});
}
// Render the treemap at the wide viewport first so its initial layout (which
// we measure against) is computed at the wide width.
await page.setViewportSize(WIDE_VIEWPORT);
const dashboard = new DashboardPage(page);
await dashboard.gotoById(dashboardId);
await dashboard.waitForLoad();
// Tab A is active on load; wait for its treemap to finish rendering.
const treemapContainer = page
.locator('[data-test-viz-type="treemap_v2"]')
.locator('[data-test="chart-container"]');
await treemapContainer.waitFor({
state: 'visible',
timeout: TIMEOUT.API_RESPONSE,
});
await dashboard.waitForChartsToLoad();
const widthsAtWide = await treemapContainer.evaluate((el: HTMLElement) => ({
offsetWidth: el.offsetWidth,
scrollWidth: el.scrollWidth,
}));
// Switch to Tab B (treemap becomes hidden), shrink the viewport so the
// available width changes while the treemap is not visible, then return.
await dashboard.switchToTopLevelTab(1);
await page.setViewportSize(NARROW_VIEWPORT);
await dashboard.switchToTopLevelTab(0);
// Let the reveal settle, mirroring the original's fixed wait.
await treemapContainer.waitFor({
state: 'visible',
timeout: TIMEOUT.API_RESPONSE,
});
await dashboard.waitForChartsToLoad();
// 1) The container itself reflowed to the narrower viewport synchronously on
// reveal. Without this the fit assertion below could pass trivially (a
// chart that never resized still has scrollWidth === offsetWidth), so this
// guards against a false green where the reflow was never exercised.
const offsetWidthAtNarrow = await treemapContainer.evaluate(
(el: HTMLElement) => el.offsetWidth,
);
expect(
offsetWidthAtNarrow,
`treemap container should narrow after the viewport shrank ` +
`(wide=${widthsAtWide.offsetWidth}, narrow=${offsetWidthAtNarrow})`,
).toBeLessThan(widthsAtWide.offsetWidth);
// 2) Once revealed, the treemap refits its container: its rendered content
// fills exactly the available width with no horizontal overflow. The
// echarts canvas re-measures a beat after the tab becomes visible, so we
// poll until it fits. A genuine resize-on-reveal regression leaves the
// chart permanently overflowing (scrollWidth > offsetWidth) and this poll
// times out red; ordinary resize latency converges and it passes.
await expect
.poll(
() =>
treemapContainer.evaluate(
(el: HTMLElement) => el.scrollWidth - el.offsetWidth,
),
{
timeout: TIMEOUT.API_RESPONSE,
message:
'treemap should refit its container (no horizontal overflow) after the tab switch',
},
)
.toBe(0);
},
);

View File

@@ -177,6 +177,28 @@ describe('Filter utils', () => {
);
});
test('Regression for #27510: formats large BIGINT column values as exact strings without precision loss', () => {
// A BIGINT column value with 16+ digits arrives as a native `bigint`
// (decoded from the `json-bigint` parse of the chart data response).
// Historically the filter UI rendered it as a raw BigNumber object
// ({ s, e, c }), throwing React error #31, or coerced it through a JS
// number and silently corrupted the trailing digits. The label
// formatter must turn it into its exact decimal string so the Select
// filter option renders correctly and the value keeps full precision.
const formatter = getDataRecordFormatter();
const largeBigInt = 1234567890125123456n;
expect(formatter(largeBigInt, GenericDataType.Numeric)).toEqual(
'1234567890125123456',
);
expect(formatter(largeBigInt, GenericDataType.String)).toEqual(
'1234567890125123456',
);
// Guard: routing the same value through a JS number (the pre-fix
// behavior) loses precision, which is exactly what broke the filter.
expect(String(Number(largeBigInt))).not.toEqual('1234567890125123456');
});
test('formatter with defined formatters returns expected values', () => {
const formatter = getDataRecordFormatter({
timeFormatter: getTimeFormatter(TimeFormats.DATABASE_DATETIME),

View File

@@ -173,6 +173,237 @@ describe('asyncEvent middleware', () => {
expect(fetchMock.callHistory.calls(CACHED_DATA_ENDPOINT)).toHaveLength(1);
});
test('backs off exponentially when polling requests keep failing', async () => {
// stop the real-timer polling loop started by beforeEach before
// switching to fake timers, so all polls run on the fake clock
mockedIsFeatureEnabled.mockReturnValueOnce(false);
asyncEvent.init(config);
jest.useFakeTimers();
try {
fetchMock.clearHistory().removeRoutes();
fetchMock.get(EVENTS_ENDPOINT, { status: 403 });
asyncEvent.init(config);
asyncEvent.waitForAsyncData(asyncPendingEvent).catch(() => {});
// first poll fires after the configured delay and fails
await jest.advanceTimersByTimeAsync(
config.GLOBAL_ASYNC_QUERIES_POLLING_DELAY,
);
expect(fetchMock.callHistory.calls(EVENTS_ENDPOINT)).toHaveLength(1);
// next poll is delayed by 2x the configured delay, so nothing yet
await jest.advanceTimersByTimeAsync(
config.GLOBAL_ASYNC_QUERIES_POLLING_DELAY,
);
expect(fetchMock.callHistory.calls(EVENTS_ENDPOINT)).toHaveLength(1);
await jest.advanceTimersByTimeAsync(
config.GLOBAL_ASYNC_QUERIES_POLLING_DELAY,
);
expect(fetchMock.callHistory.calls(EVENTS_ENDPOINT)).toHaveLength(2);
// after the second failure the delay grows to 4x
await jest.advanceTimersByTimeAsync(
3 * config.GLOBAL_ASYNC_QUERIES_POLLING_DELAY,
);
expect(fetchMock.callHistory.calls(EVENTS_ENDPOINT)).toHaveLength(2);
await jest.advanceTimersByTimeAsync(
config.GLOBAL_ASYNC_QUERIES_POLLING_DELAY,
);
expect(fetchMock.callHistory.calls(EVENTS_ENDPOINT)).toHaveLength(3);
} finally {
jest.useRealTimers();
}
});
test('resumes the configured polling delay after a successful poll', async () => {
// stop the real-timer polling loop started by beforeEach before
// switching to fake timers, so all polls run on the fake clock
mockedIsFeatureEnabled.mockReturnValueOnce(false);
asyncEvent.init(config);
jest.useFakeTimers();
try {
fetchMock.clearHistory().removeRoutes();
fetchMock.get(EVENTS_ENDPOINT, { status: 403 });
asyncEvent.init(config);
asyncEvent.waitForAsyncData(asyncPendingEvent).catch(() => {});
// two failed polls: 1x delay, then 2x delay
await jest.advanceTimersByTimeAsync(
3 * config.GLOBAL_ASYNC_QUERIES_POLLING_DELAY,
);
expect(fetchMock.callHistory.calls(EVENTS_ENDPOINT)).toHaveLength(2);
// subsequent polls succeed, resetting the backoff
fetchMock.clearHistory().removeRoutes();
fetchMock.get(EVENTS_ENDPOINT, {
status: 200,
body: { result: [] },
});
// third poll fires 4x delay after the second failure and succeeds
await jest.advanceTimersByTimeAsync(
4 * config.GLOBAL_ASYNC_QUERIES_POLLING_DELAY,
);
expect(fetchMock.callHistory.calls(EVENTS_ENDPOINT)).toHaveLength(1);
// polling is back to the configured delay
await jest.advanceTimersByTimeAsync(
config.GLOBAL_ASYNC_QUERIES_POLLING_DELAY,
);
expect(fetchMock.callHistory.calls(EVENTS_ENDPOINT)).toHaveLength(2);
} finally {
jest.useRealTimers();
}
});
test('caps the polling backoff delay at 60 seconds', async () => {
const MAX_ERROR_POLLING_DELAY_MS = 60000;
// stop the real-timer polling loop started by beforeEach before
// switching to fake timers, so all polls run on the fake clock
mockedIsFeatureEnabled.mockReturnValueOnce(false);
asyncEvent.init(config);
jest.useFakeTimers();
try {
fetchMock.clearHistory().removeRoutes();
fetchMock.get(EVENTS_ENDPOINT, { status: 403 });
asyncEvent.init(config);
asyncEvent.waitForAsyncData(asyncPendingEvent).catch(() => {});
// first poll fires after the configured delay and fails
await jest.advanceTimersByTimeAsync(
config.GLOBAL_ASYNC_QUERIES_POLLING_DELAY,
);
expect(fetchMock.callHistory.calls(EVENTS_ENDPOINT)).toHaveLength(1);
// walk the uncapped backoff: after failure N the next delay is
// 2^N times the configured delay, which stays below the cap through
// failure 10 (50ms * 2^10 = 51.2s)
for (let failures = 1; failures <= 10; failures += 1) {
await jest.advanceTimersByTimeAsync(
config.GLOBAL_ASYNC_QUERIES_POLLING_DELAY * 2 ** failures,
);
expect(fetchMock.callHistory.calls(EVENTS_ENDPOINT)).toHaveLength(
failures + 1,
);
}
// after failure 11 the uncapped delay would be 102.4s, so the cap
// takes over: no poll just before the 60s mark...
await jest.advanceTimersByTimeAsync(MAX_ERROR_POLLING_DELAY_MS - 1);
expect(fetchMock.callHistory.calls(EVENTS_ENDPOINT)).toHaveLength(11);
// ...and the next poll fires exactly at 60s
await jest.advanceTimersByTimeAsync(1);
expect(fetchMock.callHistory.calls(EVENTS_ENDPOINT)).toHaveLength(12);
// additional failures remain capped at 60s
await jest.advanceTimersByTimeAsync(MAX_ERROR_POLLING_DELAY_MS);
expect(fetchMock.callHistory.calls(EVENTS_ENDPOINT)).toHaveLength(13);
} finally {
jest.useRealTimers();
}
});
test('does not start a second loop when re-initialized during an in-flight poll', async () => {
// stop the real-timer polling loop started by beforeEach before
// switching to fake timers, so all polls run on the fake clock
mockedIsFeatureEnabled.mockReturnValueOnce(false);
asyncEvent.init(config);
jest.useFakeTimers();
try {
fetchMock.clearHistory().removeRoutes();
let resolveFetch: (response: any) => void = () => {};
fetchMock.get(
EVENTS_ENDPOINT,
new Promise(resolve => {
resolveFetch = resolve;
}),
);
asyncEvent.init(config);
asyncEvent.waitForAsyncData(asyncPendingEvent).catch(() => {});
// first poll fires and stays in-flight
await jest.advanceTimersByTimeAsync(
config.GLOBAL_ASYNC_QUERIES_POLLING_DELAY,
);
expect(fetchMock.callHistory.calls(EVENTS_ENDPOINT)).toHaveLength(1);
// re-init while that fetch is pending, then let it resolve; the
// stale invocation must not schedule a second loop
asyncEvent.init(config);
asyncEvent.waitForAsyncData(asyncPendingEvent).catch(() => {});
resolveFetch({ status: 200, body: { result: [] } });
await jest.advanceTimersByTimeAsync(0);
fetchMock.clearHistory().removeRoutes();
fetchMock.get(EVENTS_ENDPOINT, {
status: 200,
body: { result: [] },
});
// exactly one poll per delay from here on
await jest.advanceTimersByTimeAsync(
config.GLOBAL_ASYNC_QUERIES_POLLING_DELAY,
);
expect(fetchMock.callHistory.calls(EVENTS_ENDPOINT)).toHaveLength(1);
await jest.advanceTimersByTimeAsync(
config.GLOBAL_ASYNC_QUERIES_POLLING_DELAY,
);
expect(fetchMock.callHistory.calls(EVENTS_ENDPOINT)).toHaveLength(2);
} finally {
jest.useRealTimers();
}
});
test('does not resume polling when re-initialized with the feature disabled during an in-flight poll', async () => {
// stop the real-timer polling loop started by beforeEach before
// switching to fake timers, so all polls run on the fake clock
mockedIsFeatureEnabled.mockReturnValueOnce(false);
asyncEvent.init(config);
jest.useFakeTimers();
try {
fetchMock.clearHistory().removeRoutes();
let resolveFetch: (response: any) => void = () => {};
fetchMock.get(
EVENTS_ENDPOINT,
new Promise(resolve => {
resolveFetch = resolve;
}),
);
asyncEvent.init(config);
asyncEvent.waitForAsyncData(asyncPendingEvent).catch(() => {});
// first poll fires and stays in-flight
await jest.advanceTimersByTimeAsync(
config.GLOBAL_ASYNC_QUERIES_POLLING_DELAY,
);
expect(fetchMock.callHistory.calls(EVENTS_ENDPOINT)).toHaveLength(1);
// disable the feature and re-init while the fetch is pending; the
// stale invocation must not restart the stopped loop when it resumes
mockedIsFeatureEnabled.mockReturnValueOnce(false);
asyncEvent.init(config);
resolveFetch({ status: 200, body: { result: [] } });
await jest.advanceTimersByTimeAsync(0);
fetchMock.clearHistory().removeRoutes();
fetchMock.get(EVENTS_ENDPOINT, {
status: 200,
body: { result: [] },
});
await jest.advanceTimersByTimeAsync(
10 * config.GLOBAL_ASYNC_QUERIES_POLLING_DELAY,
);
expect(fetchMock.callHistory.calls(EVENTS_ENDPOINT)).toHaveLength(0);
} finally {
jest.useRealTimers();
}
});
// Regression guard for the motivating CodeQL case: a job_id that collides
// with a built-in Object property (e.g. "__proto__"/"constructor") must be
// routed through the Map-based registries without triggering prototype

View File

@@ -58,6 +58,9 @@ const LOCALSTORAGE_KEY = 'last_async_event_id';
const POLLING_URL = '/api/v1/async_event/';
const MAX_RETRIES = 6;
const RETRY_DELAY = 100;
// Cap for the exponential backoff applied when polling requests fail
// repeatedly (e.g. expired session, server or network errors)
const MAX_ERROR_POLLING_DELAY_MS = 60000;
let config: AppConfig;
let transport: string;
@@ -66,6 +69,11 @@ let pollingTimeoutId: number;
let listenersByJobId: Map<string, ListenerFn>;
let retriesByJobId: Map<string, number>;
let lastReceivedEventId: string | null | undefined;
let consecutivePollingErrorCount = 0;
// Incremented on every init() so polling invocations that are already
// awaiting a fetch when re-init happens can detect they are stale and
// stop, instead of mutating fresh state or scheduling a second loop
let pollingGeneration = 0;
const addListener = (id: string, fn: ListenerFn) => {
listenersByJobId.set(id, fn);
@@ -170,19 +178,34 @@ export const processEvents = async (events: AsyncEvent[]) => {
});
};
const getPollingDelay = () => {
if (!consecutivePollingErrorCount) return pollingDelayMs;
const backoffDelayMs = pollingDelayMs * 2 ** consecutivePollingErrorCount;
return Math.max(
pollingDelayMs,
Math.min(backoffDelayMs, MAX_ERROR_POLLING_DELAY_MS),
);
};
const loadEventsFromApi = async () => {
const generation = pollingGeneration;
const eventArgs = lastReceivedEventId ? { last_id: lastReceivedEventId } : {};
if (listenersByJobId.size) {
try {
const { result: events } = await fetchEvents(eventArgs);
if (generation !== pollingGeneration) return;
consecutivePollingErrorCount = 0;
if (events?.length) await processEvents(events);
} catch (err) {
if (generation !== pollingGeneration) return;
consecutivePollingErrorCount += 1;
logging.warn(err);
}
}
if (generation !== pollingGeneration) return;
if (transport === TRANSPORT_POLLING) {
pollingTimeoutId = window.setTimeout(loadEventsFromApi, pollingDelayMs);
pollingTimeoutId = window.setTimeout(loadEventsFromApi, getPollingDelay());
}
};
@@ -232,12 +255,14 @@ const wsConnect = (): void => {
};
export const init = (appConfig?: AppConfig) => {
if (!isFeatureEnabled(FeatureFlag.GlobalAsyncQueries)) return;
pollingGeneration += 1;
if (pollingTimeoutId) clearTimeout(pollingTimeoutId);
if (!isFeatureEnabled(FeatureFlag.GlobalAsyncQueries)) return;
listenersByJobId = new Map();
retriesByJobId = new Map();
lastReceivedEventId = null;
consecutivePollingErrorCount = 0;
config = appConfig || getBootstrapData().common.conf;
transport = config.GLOBAL_ASYNC_QUERIES_TRANSPORT || TRANSPORT_POLLING;

View File

@@ -25,14 +25,14 @@
"@types/node": "^26.1.0",
"@types/ws": "^8.18.1",
"@typescript-eslint/eslint-plugin": "^8.62.0",
"@typescript-eslint/parser": "^8.62.0",
"@typescript-eslint/parser": "^8.63.0",
"eslint": "^10.5.0",
"eslint-config-prettier": "^10.1.8",
"globals": "^17.6.0",
"prettier": "^3.9.4",
"tscw-config": "^1.1.2",
"typescript": "^6.0.3",
"typescript-eslint": "^8.62.1",
"typescript-eslint": "^8.63.0",
"vitest": "^4.1.10"
},
"engines": {
@@ -741,17 +741,17 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.62.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.62.1.tgz",
"integrity": "sha512-4EQM77WgVNxj7OkL/5b/D/xZsw00G577+UriYTC7JF5opcF3T2AuoeY7ueLaZgSVjSgCS6yOAJB5bRGLPSJUzA==",
"version": "8.63.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.63.0.tgz",
"integrity": "sha512-rvwSgqT+DHpWdzfSzPatRLm02a0GlESt++9iy3hLCDY4BgkaLcl8LBi9Yh7XGFBpwcBE/K3024QuXWTpbz4FfQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.12.2",
"@typescript-eslint/scope-manager": "8.62.1",
"@typescript-eslint/type-utils": "8.62.1",
"@typescript-eslint/utils": "8.62.1",
"@typescript-eslint/visitor-keys": "8.62.1",
"@typescript-eslint/scope-manager": "8.63.0",
"@typescript-eslint/type-utils": "8.63.0",
"@typescript-eslint/utils": "8.63.0",
"@typescript-eslint/visitor-keys": "8.63.0",
"ignore": "^7.0.5",
"natural-compare": "^1.4.0",
"ts-api-utils": "^2.5.0"
@@ -764,22 +764,22 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"@typescript-eslint/parser": "^8.62.1",
"@typescript-eslint/parser": "^8.63.0",
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/parser": {
"version": "8.62.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.62.1.tgz",
"integrity": "sha512-sPhE4iHuJDSvoAiec+Ro8JyXw8f0ql13HFR82P99nCm9GwTEKG0KYLvDe6REk8BCXuit6vJAv/Yxg5ABaNS2rA==",
"version": "8.63.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.63.0.tgz",
"integrity": "sha512-gwh4gvvlaVDKKxyfxMG+Gnu1u9X0OQBwyGLkbwB65dIzBKnxeRiJlNFqlI3zwVhNXJIs6qV7mlFCn/BIajlVig==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/scope-manager": "8.62.1",
"@typescript-eslint/types": "8.62.1",
"@typescript-eslint/typescript-estree": "8.62.1",
"@typescript-eslint/visitor-keys": "8.62.1",
"@typescript-eslint/scope-manager": "8.63.0",
"@typescript-eslint/types": "8.63.0",
"@typescript-eslint/typescript-estree": "8.63.0",
"@typescript-eslint/visitor-keys": "8.63.0",
"debug": "^4.4.3"
},
"engines": {
@@ -794,15 +794,145 @@
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/project-service": {
"version": "8.62.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.62.1.tgz",
"integrity": "sha512-yQ3RgY5RkSBpsNS1Bx/JQEcA24FOSdfGktoyprAr5u18390UQdtVcfnEv4nIrIshNnavlVyZBKxQwT1fIAE6cg==",
"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/project-service": {
"version": "8.63.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.63.0.tgz",
"integrity": "sha512-e5dh0/UI0ok53AlZ5wRkXCB32z/f2jUZqPR/ygAw5WYaSw8j9EoJWlS7wQjr/dmOaqWjnPIn2m+HhVPCMWGZVQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.62.1",
"@typescript-eslint/types": "^8.62.1",
"@typescript-eslint/tsconfig-utils": "^8.63.0",
"@typescript-eslint/types": "^8.63.0",
"debug": "^4.4.3"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/scope-manager": {
"version": "8.63.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.63.0.tgz",
"integrity": "sha512-uUyfMWCnDSN8bCpcrY8nGP2BLkQ9Xn0GsipcONcpIDWhwhO4ZSyHvyS14U3X75mzxWxL3I2UZIrenTzdzcJO8A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.63.0",
"@typescript-eslint/visitor-keys": "8.63.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.63.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.63.0.tgz",
"integrity": "sha512-sUAbkulqBAsncKnbRP3+7CtQFRKicexnj7ZwNC6ddCR7EmrXvjvdCYMJbUIqMd6lwoEriZjwLo08aS5tSjVMHg==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/types": {
"version": "8.63.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.63.0.tgz",
"integrity": "sha512-xyLtl9DUBBFrcJS4x2pIqGLH68/tC2uOa4Z7pUteW09D3bXnnXUom4dyPikzWgB7llmIc1zoeI3aoUdC4rPK/Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": {
"version": "8.63.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.63.0.tgz",
"integrity": "sha512-ygBkU+B7ex5UI/gKhaqexWev79uISfIv7XQCRNYO/jmD8rGLPyWLAb3KMRT6nd8Gt9bmUBi9+iX6tBdYfOY81Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/project-service": "8.63.0",
"@typescript-eslint/tsconfig-utils": "8.63.0",
"@typescript-eslint/types": "8.63.0",
"@typescript-eslint/visitor-keys": "8.63.0",
"debug": "^4.4.3",
"minimatch": "^10.2.2",
"semver": "^7.7.3",
"tinyglobby": "^0.2.15",
"ts-api-utils": "^2.5.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/visitor-keys": {
"version": "8.63.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.63.0.tgz",
"integrity": "sha512-UexrHGnGTpbuQHct2ExOc2ZcFbGUS9FOesCxxqdBGcpI1BxYu/LZ6U8Aq6/72XtF/qRBk9nhuGHFJIXXMhPMdw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.63.0",
"eslint-visitor-keys": "^5.0.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/parser/node_modules/eslint-visitor-keys": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
"integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": "^20.19.0 || ^22.13.0 || >=24"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/@typescript-eslint/project-service": {
"version": "8.63.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.63.0.tgz",
"integrity": "sha512-e5dh0/UI0ok53AlZ5wRkXCB32z/f2jUZqPR/ygAw5WYaSw8j9EoJWlS7wQjr/dmOaqWjnPIn2m+HhVPCMWGZVQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.63.0",
"@typescript-eslint/types": "^8.63.0",
"debug": "^4.4.3"
},
"engines": {
@@ -817,14 +947,14 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "8.62.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.62.1.tgz",
"integrity": "sha512-r4d249KbQ1SFdpeStvob8Ih6aPPIzfqllPVOtvhve6ZcpuVcYo5/7zUWckKpHE7StASX4kTKZTLf0WQm/wPkcg==",
"version": "8.63.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.63.0.tgz",
"integrity": "sha512-uUyfMWCnDSN8bCpcrY8nGP2BLkQ9Xn0GsipcONcpIDWhwhO4ZSyHvyS14U3X75mzxWxL3I2UZIrenTzdzcJO8A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.62.1",
"@typescript-eslint/visitor-keys": "8.62.1"
"@typescript-eslint/types": "8.63.0",
"@typescript-eslint/visitor-keys": "8.63.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -835,9 +965,9 @@
}
},
"node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.62.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.62.1.tgz",
"integrity": "sha512-xadytJqX9vJVQ2fdQjkcIVigwaOJNWkpjdLt6cEQ+xPnrI1fkp+/jZE/I97k9KUjqtpd25i0HeyZf3T6dutv2g==",
"version": "8.63.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.63.0.tgz",
"integrity": "sha512-sUAbkulqBAsncKnbRP3+7CtQFRKicexnj7ZwNC6ddCR7EmrXvjvdCYMJbUIqMd6lwoEriZjwLo08aS5tSjVMHg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -852,15 +982,15 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "8.62.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.62.1.tgz",
"integrity": "sha512-aXM5xlqXiTxPibXB93cLAURfT3rlizf7uMXISCXy66Isr/9hISJx3yDsKl0L7lKa51b8JpFuNKby0/O0pEm9jg==",
"version": "8.63.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.63.0.tgz",
"integrity": "sha512-Nzzh/OGxVCOjObjaj1CQF2RUasyYy2Jfuh+zZ3PjLzG2fYRriAiZLib9UKtO+CpQAS3YHiAS+ckZDclwqI1TPA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.62.1",
"@typescript-eslint/typescript-estree": "8.62.1",
"@typescript-eslint/utils": "8.62.1",
"@typescript-eslint/types": "8.63.0",
"@typescript-eslint/typescript-estree": "8.63.0",
"@typescript-eslint/utils": "8.63.0",
"debug": "^4.4.3",
"ts-api-utils": "^2.5.0"
},
@@ -877,9 +1007,9 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "8.62.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.62.1.tgz",
"integrity": "sha512-ooCzJFaf+Hg+uG6fA3NRFGuFjlfNlDhBthbv4ZPU/0elCAFUfnyXUvf/WOpHz/jYwSmvU2GkR2LtyUfy1AxZ1Q==",
"version": "8.63.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.63.0.tgz",
"integrity": "sha512-xyLtl9DUBBFrcJS4x2pIqGLH68/tC2uOa4Z7pUteW09D3bXnnXUom4dyPikzWgB7llmIc1zoeI3aoUdC4rPK/Q==",
"dev": true,
"license": "MIT",
"engines": {
@@ -891,16 +1021,16 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "8.62.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.62.1.tgz",
"integrity": "sha512-xMcW9oP9u7fAMXYs9A65CVmtLQe2r//oXINHfi8HV+oiqhih17sbLdhXr4540YWlgpDKQdY854OL5ZrdCiQsAA==",
"version": "8.63.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.63.0.tgz",
"integrity": "sha512-ygBkU+B7ex5UI/gKhaqexWev79uISfIv7XQCRNYO/jmD8rGLPyWLAb3KMRT6nd8Gt9bmUBi9+iX6tBdYfOY81Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/project-service": "8.62.1",
"@typescript-eslint/tsconfig-utils": "8.62.1",
"@typescript-eslint/types": "8.62.1",
"@typescript-eslint/visitor-keys": "8.62.1",
"@typescript-eslint/project-service": "8.63.0",
"@typescript-eslint/tsconfig-utils": "8.63.0",
"@typescript-eslint/types": "8.63.0",
"@typescript-eslint/visitor-keys": "8.63.0",
"debug": "^4.4.3",
"minimatch": "^10.2.2",
"semver": "^7.7.3",
@@ -919,16 +1049,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
"version": "8.62.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.62.1.tgz",
"integrity": "sha512-sHtbPfuKNZCG+ih8SyjjucqRntSVmp8XgL5u6o9mAhiSn8ds5o/M/XdM0abweme2Tln3szOstOrZ9OXitvPh0g==",
"version": "8.63.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.63.0.tgz",
"integrity": "sha512-fUKaeAvrTuQg/Tgt3nliAUSZHJM6DlCcfyEmxCvlX8kieWSStBX+5O5Fnidtc3i2JrH+9c/GL4RY2iasd/GPTA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.9.1",
"@typescript-eslint/scope-manager": "8.62.1",
"@typescript-eslint/types": "8.62.1",
"@typescript-eslint/typescript-estree": "8.62.1"
"@typescript-eslint/scope-manager": "8.63.0",
"@typescript-eslint/types": "8.63.0",
"@typescript-eslint/typescript-estree": "8.63.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -943,13 +1073,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "8.62.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.62.1.tgz",
"integrity": "sha512-4g3BLxfdTMy8iZG0MaBkadnlRrCJ74cQiFbyEVMrkwIoqdyaXXQM22cotDvrl4x28wgIZ9rEJRoM+mmhSJpJ1g==",
"version": "8.63.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.63.0.tgz",
"integrity": "sha512-UexrHGnGTpbuQHct2ExOc2ZcFbGUS9FOesCxxqdBGcpI1BxYu/LZ6U8Aq6/72XtF/qRBk9nhuGHFJIXXMhPMdw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.62.1",
"@typescript-eslint/types": "8.63.0",
"eslint-visitor-keys": "^5.0.0"
},
"engines": {
@@ -2861,16 +2991,41 @@
}
},
"node_modules/typescript-eslint": {
"version": "8.62.1",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.62.1.tgz",
"integrity": "sha512-vymnnM5g0AKQDSAyfP12nMIBvgwgA42syg74kkuZ4x1VuTzwQKwc5h9rGxeShCjny5o+zWAb6OEoz7XLgrIkIw==",
"version": "8.63.0",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.63.0.tgz",
"integrity": "sha512-xgwXyzG4sK9ALkBxbyGkTMMOS+imnW65iPhxCQMK83KhxyoDNW7l+IDqEf9vMdoUidHpOoS967RCq4eMiTexwQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/eslint-plugin": "8.62.1",
"@typescript-eslint/parser": "8.62.1",
"@typescript-eslint/eslint-plugin": "8.63.0",
"@typescript-eslint/parser": "8.63.0",
"@typescript-eslint/typescript-estree": "8.63.0",
"@typescript-eslint/utils": "8.63.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/typescript-eslint/node_modules/@typescript-eslint/parser": {
"version": "8.62.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.62.1.tgz",
"integrity": "sha512-sPhE4iHuJDSvoAiec+Ro8JyXw8f0ql13HFR82P99nCm9GwTEKG0KYLvDe6REk8BCXuit6vJAv/Yxg5ABaNS2rA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/scope-manager": "8.62.1",
"@typescript-eslint/types": "8.62.1",
"@typescript-eslint/typescript-estree": "8.62.1",
"@typescript-eslint/utils": "8.62.1"
"@typescript-eslint/visitor-keys": "8.62.1",
"debug": "^4.4.3"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"

View File

@@ -34,14 +34,14 @@
"@types/node": "^26.1.0",
"@types/ws": "^8.18.1",
"@typescript-eslint/eslint-plugin": "^8.62.0",
"@typescript-eslint/parser": "^8.62.0",
"@typescript-eslint/parser": "^8.63.0",
"eslint": "^10.5.0",
"eslint-config-prettier": "^10.1.8",
"globals": "^17.6.0",
"prettier": "^3.9.4",
"tscw-config": "^1.1.2",
"typescript": "^6.0.3",
"typescript-eslint": "^8.62.1",
"typescript-eslint": "^8.63.0",
"vitest": "^4.1.10"
},
"engines": {

View File

@@ -417,12 +417,12 @@ class SupersetShillelaghAdapter(Adapter):
query = self._build_sql(bounds, order, limit, offset)
with self.engine_context() as engine:
connection = engine.connect()
rows = connection.execute(query)
for i, row in enumerate(rows):
data = dict(zip(self.columns, row, strict=False))
data["rowid"] = data[self._rowid] if self._rowid else i
yield data
with engine.connect() as connection:
rows = connection.execute(query)
for i, row in enumerate(rows):
data = dict(zip(self.columns, row, strict=False))
data["rowid"] = data[self._rowid] if self._rowid else i
yield data
@check_dml
def insert_row(self, row: Row) -> int:
@@ -445,15 +445,16 @@ class SupersetShillelaghAdapter(Adapter):
query = self._table.insert().values(**row)
with self.engine_context() as engine:
connection = engine.connect()
result = connection.execute(query)
with engine.begin() as connection:
result = connection.execute(query)
# return rowid
if self._rowid:
return result.inserted_primary_key[0]
# return rowid
if self._rowid:
return result.inserted_primary_key[0]
query = select(func.count()).select_from(self._table)
return connection.execute(query).scalar()
with engine.connect() as connection:
return connection.execute(query).scalar()
@check_dml
@has_rowid
@@ -464,8 +465,8 @@ class SupersetShillelaghAdapter(Adapter):
query = self._table.delete().where(self._table.c[self._rowid] == row_id)
with self.engine_context() as engine:
connection = engine.connect()
connection.execute(query)
with engine.begin() as connection:
connection.execute(query)
@check_dml
@has_rowid
@@ -488,5 +489,5 @@ class SupersetShillelaghAdapter(Adapter):
)
with self.engine_context() as engine:
connection = engine.connect()
connection.execute(query)
with engine.begin() as connection:
connection.execute(query)

View File

@@ -55,6 +55,52 @@ from superset.sql.parse import SQLScript
logger = logging.getLogger(__name__)
async def _validate_non_destructive_sql(
request: ExecuteSqlRequest,
ctx: Context,
database: Any,
sql_preview: str,
) -> ExecuteSqlResponse | None:
"""Return an error response when SQL cannot safely be executed."""
with event_logger.log_context(action="mcp.execute_sql.ddl_check"):
try:
sql_to_check: str = request.sql
if request.template_params:
from superset.jinja_context import get_template_processor
tp = get_template_processor(database=database)
sql_to_check = tp.process_template(
request.sql, **request.template_params
)
script = SQLScript(sql_to_check, database.db_engine_spec.engine)
if script.has_destructive():
await ctx.error("Destructive DDL blocked: sql_preview=%r" % sql_preview)
return ExecuteSqlResponse(
success=False,
error=(
"Destructive DDL statements (DROP, TRUNCATE, ALTER) "
"are not allowed through MCP. Use the Superset SQL "
"Lab UI for administrative database operations."
),
error_type=SupersetErrorType.DML_NOT_ALLOWED_ERROR.value,
)
except Exception as parse_err:
await ctx.error(
"DDL pre-check failed to parse SQL, blocking query: %s" % str(parse_err)
)
return ExecuteSqlResponse(
success=False,
error=(
"SQL could not be parsed for security validation. "
"Please check your SQL syntax and try again."
),
error_type=SupersetErrorType.INVALID_SQL_ERROR.value,
)
return None
@tool(
tags=["mutate"],
class_permission_name="SQLLab",
@@ -122,44 +168,11 @@ async def execute_sql(request: ExecuteSqlRequest, ctx: Context) -> ExecuteSqlRes
# Fail-closed: if parsing fails, block the query rather than
# allowing potentially destructive SQL to bypass the check.
# Render Jinja2 templates first so templated SQL can be parsed.
with event_logger.log_context(action="mcp.execute_sql.ddl_check"):
try:
sql_to_check = request.sql
if request.template_params:
from superset.jinja_context import get_template_processor
tp = get_template_processor(database=database)
sql_to_check = tp.process_template(
request.sql, **request.template_params
)
script = SQLScript(sql_to_check, database.db_engine_spec.engine)
if script.has_destructive():
await ctx.error(
"Destructive DDL blocked: sql_preview=%r" % sql_preview
)
return ExecuteSqlResponse(
success=False,
error=(
"Destructive DDL statements (DROP, TRUNCATE, ALTER) "
"are not allowed through MCP. Use the Superset SQL "
"Lab UI for administrative database operations."
),
error_type=SupersetErrorType.DML_NOT_ALLOWED_ERROR.value,
)
except Exception as parse_err:
await ctx.error(
"DDL pre-check failed to parse SQL, blocking query: %s"
% str(parse_err)
)
return ExecuteSqlResponse(
success=False,
error=(
"SQL could not be parsed for security validation. "
"Please check your SQL syntax and try again."
),
error_type=SupersetErrorType.INVALID_SQL_ERROR.value,
)
validation_error: (
ExecuteSqlResponse | None
) = await _validate_non_destructive_sql(request, ctx, database, sql_preview)
if validation_error is not None:
return validation_error
# 3. Build QueryOptions and execute query
cache_opts = CacheOptions(force_refresh=True) if request.force_refresh else None

View File

@@ -121,12 +121,13 @@ def test_time_grain_expressions(dttm: str, grain: str, expected: str) -> None:
from superset.db_engine_specs.sqlite import SqliteEngineSpec
engine = create_engine("sqlite://")
connection = engine.connect()
connection.execute(text("CREATE TABLE t (dttm DATETIME)"))
connection.execute(text("INSERT INTO t VALUES (:dttm)"), {"dttm": dttm})
with engine.begin() as connection:
connection.execute(text("CREATE TABLE t (dttm DATETIME)"))
connection.execute(text("INSERT INTO t VALUES (:dttm)"), {"dttm": dttm})
# pylint: disable=protected-access
expression = SqliteEngineSpec._time_grain_expressions[grain].format(col="dttm")
sql = f"SELECT {expression} FROM t" # noqa: S608
result = connection.execute(text(sql)).scalar()
with engine.connect() as connection:
result = connection.execute(text(sql)).scalar()
assert result == expected

View File

@@ -63,16 +63,17 @@ def database1(session: Session) -> Iterator["Database"]:
@pytest.fixture
def table1(session: Session, database1: "Database") -> Iterator[None]:
with database1.get_sqla_engine() as engine:
conn = engine.connect()
conn.execute(
text("CREATE TABLE table1 (a INTEGER NOT NULL PRIMARY KEY, b INTEGER)")
)
conn.execute(text("INSERT INTO table1 (a, b) VALUES (1, 10), (2, 20)"))
with engine.begin() as conn:
conn.execute(
text("CREATE TABLE table1 (a INTEGER NOT NULL PRIMARY KEY, b INTEGER)")
)
conn.execute(text("INSERT INTO table1 (a, b) VALUES (1, 10), (2, 20)"))
db.session.commit()
yield
conn.execute(text("DROP TABLE table1"))
with engine.begin() as conn:
conn.execute(text("DROP TABLE table1"))
db.session.commit()
@@ -99,16 +100,19 @@ def database2(session: Session) -> Iterator["Database"]:
@pytest.fixture
def table2(session: Session, database2: "Database") -> Iterator[None]:
with database2.get_sqla_engine() as engine:
conn = engine.connect()
conn.execute(
text("CREATE TABLE table2 (a INTEGER NOT NULL PRIMARY KEY, b TEXT)")
)
conn.execute(text("INSERT INTO table2 (a, b) VALUES (1, 'ten'), (2, 'twenty')"))
with engine.begin() as conn:
conn.execute(
text("CREATE TABLE table2 (a INTEGER NOT NULL PRIMARY KEY, b TEXT)")
)
conn.execute(
text("INSERT INTO table2 (a, b) VALUES (1, 'ten'), (2, 'twenty')")
)
db.session.commit()
yield
conn.execute(text("DROP TABLE table2"))
with engine.begin() as conn:
conn.execute(text("DROP TABLE table2"))
db.session.commit()
@@ -137,9 +141,9 @@ def test_superset(mocker: MockerFixture, app_context: None, table1: None) -> Non
# Skip test if superset:// dialect can't be loaded (common in Docker)
pytest.skip(f"Superset dialect not available: {e}")
conn = engine.connect()
results = conn.execute(text('SELECT * FROM "database1.table1"'))
assert list(results) == [(1, 10), (2, 20)]
with engine.connect() as conn:
results = conn.execute(text('SELECT * FROM "database1.table1"'))
assert list(results) == [(1, 10), (2, 20)]
@with_config(
@@ -178,9 +182,9 @@ def test_superset_limit(mocker: MockerFixture, app_context: None, table1: None)
# Skip test if superset:// dialect can't be loaded (common in Docker)
pytest.skip(f"Superset dialect not available: {e}")
conn = engine.connect()
results = conn.execute(text('SELECT * FROM "database1.table1"'))
assert list(results) == [(1, 10)]
with engine.connect() as conn:
results = conn.execute(text('SELECT * FROM "database1.table1"'))
assert list(results) == [(1, 10)]
@with_feature_flags(ENABLE_SUPERSET_META_DB=True)
@@ -213,16 +217,16 @@ def test_superset_joins(
# Skip test if superset:// dialect can't be loaded (common in Docker)
pytest.skip(f"Superset dialect not available: {e}")
conn = engine.connect()
results = conn.execute(
text("""
SELECT t1.b, t2.b
FROM "database1.table1" AS t1
JOIN "database2.table2" AS t2
ON t1.a = t2.a
""")
)
assert list(results) == [(10, "ten"), (20, "twenty")]
with engine.connect() as conn:
results = conn.execute(
text("""
SELECT t1.b, t2.b
FROM "database1.table1" AS t1
JOIN "database2.table2" AS t2
ON t1.a = t2.a
""")
)
assert list(results) == [(10, "ten"), (20, "twenty")]
@with_feature_flags(ENABLE_SUPERSET_META_DB=True)
@@ -257,22 +261,27 @@ def test_dml(
# Skip test if superset:// dialect can't be loaded (common in Docker)
pytest.skip(f"Superset dialect not available: {e}")
conn = engine.connect()
with engine.begin() as conn:
conn.execute(text('INSERT INTO "database1.table1" (a, b) VALUES (3, 30)'))
with engine.connect() as conn:
results = conn.execute(text('SELECT * FROM "database1.table1"'))
assert list(results) == [(1, 10), (2, 20), (3, 30)]
with engine.begin() as conn:
conn.execute(text('UPDATE "database1.table1" SET b=35 WHERE a=3'))
with engine.connect() as conn:
results = conn.execute(text('SELECT * FROM "database1.table1"'))
assert list(results) == [(1, 10), (2, 20), (3, 35)]
with engine.begin() as conn:
conn.execute(text('DELETE FROM "database1.table1" WHERE b>20'))
with engine.connect() as conn:
results = conn.execute(text('SELECT * FROM "database1.table1"'))
assert list(results) == [(1, 10), (2, 20)]
conn.execute(text('INSERT INTO "database1.table1" (a, b) VALUES (3, 30)'))
results = conn.execute(text('SELECT * FROM "database1.table1"'))
assert list(results) == [(1, 10), (2, 20), (3, 30)]
conn.execute(text('UPDATE "database1.table1" SET b=35 WHERE a=3'))
results = conn.execute(text('SELECT * FROM "database1.table1"'))
assert list(results) == [(1, 10), (2, 20), (3, 35)]
conn.execute(text('DELETE FROM "database1.table1" WHERE b>20'))
results = conn.execute(text('SELECT * FROM "database1.table1"'))
assert list(results) == [(1, 10), (2, 20)]
with pytest.raises(ProgrammingError) as excinfo:
conn.execute(
text("""INSERT INTO "database2.table2" (a, b) VALUES (3, 'thirty')""")
)
with engine.begin() as conn:
with pytest.raises(ProgrammingError) as excinfo:
conn.execute(
text("""INSERT INTO "database2.table2" (a, b) VALUES (3, 'thirty')""")
)
assert str(excinfo.value).strip() == (
"(shillelagh.exceptions.ProgrammingError) DML not enabled in database "
'"database2"\n[SQL: INSERT INTO "database2.table2" (a, b) '
@@ -324,9 +333,9 @@ def test_security_manager(
# Skip test if superset:// dialect can't be loaded (common in Docker)
pytest.skip(f"Superset dialect not available: {e}")
conn = engine.connect()
with pytest.raises(SupersetSecurityException) as excinfo:
conn.execute(text('SELECT * FROM "database1.table1"'))
with engine.connect() as conn:
with pytest.raises(SupersetSecurityException) as excinfo:
conn.execute(text('SELECT * FROM "database1.table1"'))
assert str(excinfo.value) == (
"You need access to the following tables: `table1`,\n "
"`all_database_access` or `all_datasource_access` permission"
@@ -358,13 +367,13 @@ def test_allowed_dbs(mocker: MockerFixture, app_context: None, table1: None) ->
# Skip test if superset:// dialect can't be loaded (common in Docker)
pytest.skip(f"Superset dialect not available: {e}")
conn = engine.connect()
with engine.connect() as conn:
results = conn.execute(text('SELECT * FROM "database1.table1"'))
assert list(results) == [(1, 10), (2, 20)]
results = conn.execute(text('SELECT * FROM "database1.table1"'))
assert list(results) == [(1, 10), (2, 20)]
with pytest.raises(ProgrammingError) as excinfo:
conn.execute(text('SELECT * FROM "database2.table2"'))
with engine.connect() as conn:
with pytest.raises(ProgrammingError) as excinfo:
conn.execute(text('SELECT * FROM "database2.table2"'))
assert str(excinfo.value) == (
"""
(shillelagh.exceptions.ProgrammingError) Unsupported table: database2.table2

View File

@@ -103,7 +103,7 @@ def _indexes(engine: Engine) -> set[str]:
def test_upgrade_adds_deleted_at_column_and_index(engine: Engine) -> None:
with engine.connect() as conn:
with engine.begin() as conn:
ctx = MigrationContext.configure(conn)
with Operations.context(ctx):
migration.upgrade()
@@ -115,7 +115,7 @@ def test_upgrade_adds_deleted_at_column_and_index(engine: Engine) -> None:
def test_downgrade_drops_deleted_at_column_and_index(engine: Engine) -> None:
with engine.connect() as conn:
with engine.begin() as conn:
ctx = MigrationContext.configure(conn)
with Operations.context(ctx):
migration.upgrade()
@@ -138,7 +138,7 @@ def test_downgrade_preserves_soft_deleted_row_data(engine: Engine) -> None:
hard-delete, restore, or rename BEFORE downgrading. See the
"Rollback note" in ``UPDATING.md``.
"""
with engine.connect() as conn:
with engine.begin() as conn:
ctx = MigrationContext.configure(conn)
with Operations.context(ctx):
migration.upgrade()
@@ -198,7 +198,7 @@ def test_sqlite_slug_constraint_swap_is_a_no_op(engine: Engine) -> None:
"pre-condition: legacy slug constraint must exist before the migration runs"
)
with engine.connect() as conn:
with engine.begin() as conn:
ctx = MigrationContext.configure(conn)
with Operations.context(ctx):
migration.upgrade()
@@ -207,7 +207,7 @@ def test_sqlite_slug_constraint_swap_is_a_no_op(engine: Engine) -> None:
"upgrade() must keep the legacy slug constraint on SQLite (no-op branch)"
)
with engine.connect() as conn:
with engine.begin() as conn:
ctx = MigrationContext.configure(conn)
with Operations.context(ctx):
migration.downgrade()
@@ -222,7 +222,7 @@ def test_upgrade_is_idempotent(engine: Engine) -> None:
idempotent skip-if-exists; running ``upgrade()`` twice must not
raise.
"""
with engine.connect() as conn:
with engine.begin() as conn:
ctx = MigrationContext.configure(conn)
with Operations.context(ctx):
migration.upgrade()
@@ -233,7 +233,7 @@ def test_downgrade_is_idempotent(engine: Engine) -> None:
"""``drop_columns`` / ``drop_index`` are skip-if-not-exists; running
``downgrade()`` twice must not raise.
"""
with engine.connect() as conn:
with engine.begin() as conn:
ctx = MigrationContext.configure(conn)
with Operations.context(ctx):
migration.upgrade()

View File

@@ -88,7 +88,7 @@ def _indexes(engine: Engine) -> set[str]:
def test_upgrade_adds_deleted_at_column_and_index(engine: Engine) -> None:
"""upgrade() adds the nullable ``deleted_at`` column and its index."""
with engine.connect() as conn:
with engine.begin() as conn:
ctx = MigrationContext.configure(conn)
with Operations.context(ctx):
migration.upgrade()
@@ -101,7 +101,7 @@ def test_upgrade_adds_deleted_at_column_and_index(engine: Engine) -> None:
def test_downgrade_drops_deleted_at_column_and_index(engine: Engine) -> None:
"""downgrade() removes both schema artifacts (column and index)."""
with engine.connect() as conn:
with engine.begin() as conn:
ctx = MigrationContext.configure(conn)
with Operations.context(ctx):
migration.upgrade()
@@ -124,7 +124,7 @@ def test_downgrade_preserves_soft_deleted_row_data(engine: Engine) -> None:
hard-delete, restore, or rename BEFORE downgrading. See the
"Rollback note" in ``UPDATING.md``.
"""
with engine.connect() as conn:
with engine.begin() as conn:
ctx = MigrationContext.configure(conn)
with Operations.context(ctx):
migration.upgrade()
@@ -171,7 +171,7 @@ def test_upgrade_is_idempotent(engine: Engine) -> None:
idempotent skip-if-exists; running ``upgrade()`` twice must not
raise.
"""
with engine.connect() as conn:
with engine.begin() as conn:
ctx = MigrationContext.configure(conn)
with Operations.context(ctx):
migration.upgrade()
@@ -182,7 +182,7 @@ def test_downgrade_is_idempotent(engine: Engine) -> None:
"""``drop_columns`` / ``drop_index`` are skip-if-not-exists; running
``downgrade()`` twice must not raise.
"""
with engine.connect() as conn:
with engine.begin() as conn:
ctx = MigrationContext.configure(conn)
with Operations.context(ctx):
migration.upgrade()

View File

@@ -85,7 +85,7 @@ def _indexes(engine: Engine) -> set[str]:
def test_upgrade_adds_deleted_at_column_and_index(engine: Engine) -> None:
with engine.connect() as conn:
with engine.begin() as conn:
ctx = MigrationContext.configure(conn)
with Operations.context(ctx):
migration.upgrade()
@@ -97,7 +97,7 @@ def test_upgrade_adds_deleted_at_column_and_index(engine: Engine) -> None:
def test_downgrade_drops_deleted_at_column_and_index(engine: Engine) -> None:
with engine.connect() as conn:
with engine.begin() as conn:
ctx = MigrationContext.configure(conn)
with Operations.context(ctx):
migration.upgrade()
@@ -120,7 +120,7 @@ def test_downgrade_preserves_soft_deleted_row_data(engine: Engine) -> None:
hard-delete, restore, or rename BEFORE downgrading. See the
"Rollback note" in ``UPDATING.md``.
"""
with engine.connect() as conn:
with engine.begin() as conn:
ctx = MigrationContext.configure(conn)
with Operations.context(ctx):
migration.upgrade()
@@ -170,7 +170,7 @@ def test_upgrade_is_idempotent(engine: Engine) -> None:
idempotent skip-if-exists; running ``upgrade()`` twice must not
raise.
"""
with engine.connect() as conn:
with engine.begin() as conn:
ctx = MigrationContext.configure(conn)
with Operations.context(ctx):
migration.upgrade()
@@ -181,7 +181,7 @@ def test_downgrade_is_idempotent(engine: Engine) -> None:
"""``drop_columns`` / ``drop_index`` are skip-if-not-exists; running
``downgrade()`` twice must not raise.
"""
with engine.connect() as conn:
with engine.begin() as conn:
ctx = MigrationContext.configure(conn)
with Operations.context(ctx):
migration.upgrade()

View File

@@ -3491,6 +3491,7 @@ def test_map_query_object_shifts_time_offset_via_temporal_range_filter(
main_query, offset_query = queries[0], queries[1]
def time_filter_bounds(query: SemanticQuery) -> tuple[datetime, datetime]:
assert query.filters is not None
gte = next(
f.value
for f in query.filters
@@ -3505,6 +3506,8 @@ def test_map_query_object_shifts_time_offset_via_temporal_range_filter(
and f.column.name == "order_date"
and f.operator == Operator.LESS_THAN
)
assert isinstance(gte, datetime)
assert isinstance(lt, datetime)
return gte, lt
main_gte, main_lt = time_filter_bounds(main_query)