"
]
@@ -2501,7 +2501,21 @@
"cell_type": "code",
"execution_count": 49,
"metadata": {},
- "outputs": [],
+ "outputs": [
+ {
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "/var/folders/34/tmcyrs6n68xb9whcgvwfpnkr0000gn/T/ipykernel_56053/784227429.py:1: FutureWarning: A value is trying to be set on a copy of a DataFrame or Series through chained assignment using an inplace method.\n",
+ "The behavior will change in pandas 3.0. This inplace method will never work because the intermediate object on which we are setting values always behaves as a copy.\n",
+ "\n",
+ "For example, when doing 'df[col].method(value, inplace=True)', try using 'df.method({col: value}, inplace=True)' or df[col] = df[col].method(value) instead, to perform the operation inplace on the original object.\n",
+ "\n",
+ "\n",
+ " philippines_regions['name'].replace({'Dinagat Islands (Region XIII)': 'Caraga Administrative Region (Region XIII)',\n"
+ ]
+ }
+ ],
"source": [
"philippines_regions['name'] = philippines_regions['name'].replace({\n",
" 'Dinagat Islands (Region XIII)': 'Caraga Administrative Region (Region XIII)',\n",
@@ -2526,7 +2540,7 @@
},
{
"data": {
- "image/png": "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",
+ "image/png": "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",
"text/plain": [
"
,
- );
- expect(menuWrapper.find(Menu.Item)).toHaveLength(2);
});
- it('should render health check message', () => {
- const wrapper = setup();
- expect(wrapper.find(Icons.AlertSolid)).toExist();
- const tooltip = wrapper.find(Tooltip).at(0);
- expect(tooltip.prop('title')).toBe(
- defaultProps.datasource.health_check_message,
- );
+ it('should render health check message', async () => {
+ setup();
+ const modalTrigger = screen.getByLabelText('alert-solid');
+ expect(modalTrigger).toBeInTheDocument();
+
+ // Hover the modal so healthcheck message can show up
+ fireEvent.mouseOver(modalTrigger);
+ await waitFor(() => {
+ expect(
+ screen.getByText(defaultProps.datasource.health_check_message),
+ ).toBeInTheDocument();
+ });
});
it('Gets Datasource Title', () => {
diff --git a/superset-frontend/src/explore/components/controls/DatasourceControl/index.jsx b/superset-frontend/src/explore/components/controls/DatasourceControl/index.jsx
index 7b3961853a4c9..90f8b5d99e4d7 100644
--- a/superset-frontend/src/explore/components/controls/DatasourceControl/index.jsx
+++ b/superset-frontend/src/explore/components/controls/DatasourceControl/index.jsx
@@ -136,7 +136,7 @@ const SAVE_AS_DATASET = 'save_as_dataset';
// a tooltip for user can see the full name by hovering over the visually truncated string in UI
const VISIBLE_TITLE_LENGTH = 25;
-// Assign icon for each DatasourceType. If no icon assingment is found in the lookup, no icon will render
+// Assign icon for each DatasourceType. If no icon assignment is found in the lookup, no icon will render
export const datasourceIconLookup = {
[DatasourceType.Query]: (
@@ -188,7 +188,7 @@ class DatasourceControl extends PureComponent {
const { columns } = datasource;
// the current granularity_sqla might not be a temporal column anymore
const timeCol = this.props.form_data?.granularity_sqla;
- const isGranularitySqalTemporal = columns.find(
+ const isGranularitySqlaTemporal = columns.find(
({ column_name }) => column_name === timeCol,
)?.is_dttm;
// the current main_dttm_col might not be a temporal column anymore
@@ -198,7 +198,7 @@ class DatasourceControl extends PureComponent {
// if the current granularity_sqla is empty or it is not a temporal column anymore
// let's update the control value
- if (datasource.type === 'table' && !isGranularitySqalTemporal) {
+ if (datasource.type === 'table' && !isGranularitySqlaTemporal) {
const temporalColumn = isDefaultTemporal
? defaultTemporalColumn
: temporalColumns?.[0];
diff --git a/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterEditPopoverSimpleTabContent/AdhocFilterEditPopoverSimpleTabContent.test.tsx b/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterEditPopoverSimpleTabContent/AdhocFilterEditPopoverSimpleTabContent.test.tsx
index 1e352cc38d3a0..4bbc3af2f6cab 100644
--- a/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterEditPopoverSimpleTabContent/AdhocFilterEditPopoverSimpleTabContent.test.tsx
+++ b/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterEditPopoverSimpleTabContent/AdhocFilterEditPopoverSimpleTabContent.test.tsx
@@ -18,7 +18,7 @@
*/
import * as redux from 'react-redux';
import sinon from 'sinon';
-import { shallow } from 'enzyme';
+import { render, screen, act, waitFor } from 'spec/helpers/testing-library';
import thunk from 'redux-thunk';
import { Provider } from 'react-redux';
import configureStore from 'redux-mock-store';
@@ -30,7 +30,6 @@ import {
OPERATOR_ENUM_TO_OPERATOR_TYPE,
} from 'src/explore/constants';
import AdhocMetric from 'src/explore/components/controls/MetricControl/AdhocMetric';
-import { render, screen, act, waitFor } from '@testing-library/react';
import { supersetTheme, FeatureFlag, ThemeProvider } from '@superset-ui/core';
import * as uiCore from '@superset-ui/core';
import userEvent from '@testing-library/user-event';
@@ -133,20 +132,17 @@ function setup(overrides?: Record) {
...overrides,
validHandler,
};
- const wrapper = shallow(
- ,
- );
- return { wrapper, props };
+ render();
+ return props;
}
describe('AdhocFilterEditPopoverSimpleTabContent', () => {
- it('renders the simple tab form', () => {
- const { wrapper } = setup();
- expect(wrapper).toExist();
+ it('can render the simple tab form', () => {
+ expect(() => setup()).not.toThrow();
});
it('shows boolean only operators when subject is boolean', () => {
- const { props } = setup({
+ const props = setup({
adhocFilter: new AdhocFilter({
expressionType: ExpressionTypes.Simple,
subject: 'value',
@@ -174,7 +170,7 @@ describe('AdhocFilterEditPopoverSimpleTabContent', () => {
].map(operator => expect(isOperatorRelevant(operator, 'value')).toBe(true));
});
it('shows boolean only operators when subject is number', () => {
- const { props } = setup({
+ const props = setup({
adhocFilter: new AdhocFilter({
expressionType: ExpressionTypes.Simple,
subject: 'value',
@@ -203,7 +199,7 @@ describe('AdhocFilterEditPopoverSimpleTabContent', () => {
});
it('will convert from individual comparator to array if the operator changes to multi', () => {
- const { props } = setup();
+ const props = setup();
const { onOperatorChange } = useSimpleTabFilterProps(props);
onOperatorChange(Operators.In);
expect(props.onChange.calledOnce).toBe(true);
@@ -212,7 +208,7 @@ describe('AdhocFilterEditPopoverSimpleTabContent', () => {
});
it('will convert from array to individual comparators if the operator changes from multi', () => {
- const { props } = setup({
+ const props = setup({
adhocFilter: simpleMultiAdhocFilter,
});
const { onOperatorChange } = useSimpleTabFilterProps(props);
@@ -228,7 +224,7 @@ describe('AdhocFilterEditPopoverSimpleTabContent', () => {
});
it('passes the new adhocFilter to onChange after onComparatorChange', () => {
- const { props } = setup();
+ const props = setup();
const { onComparatorChange } = useSimpleTabFilterProps(props);
onComparatorChange('20');
expect(props.onChange.calledOnce).toBe(true);
@@ -238,13 +234,13 @@ describe('AdhocFilterEditPopoverSimpleTabContent', () => {
});
it('will filter operators for table datasources', () => {
- const { props } = setup({ datasource: { type: 'table' } });
+ const props = setup({ datasource: { type: 'table' } });
const { isOperatorRelevant } = useSimpleTabFilterProps(props);
expect(isOperatorRelevant(Operators.Like, 'value')).toBe(true);
});
it('will show LATEST PARTITION operator', () => {
- const { props } = setup({
+ const props = setup({
datasource: {
type: 'table',
datasource_name: 'table1',
@@ -263,7 +259,7 @@ describe('AdhocFilterEditPopoverSimpleTabContent', () => {
expressionType: ExpressionTypes.Simple,
subject: 'ds',
});
- const { props } = setup({
+ const props = setup({
datasource: {
type: 'table',
datasource_name: 'table1',
@@ -288,7 +284,7 @@ describe('AdhocFilterEditPopoverSimpleTabContent', () => {
);
});
it('will not display boolean operators when column type is string', () => {
- const { props } = setup({
+ const props = setup({
datasource: {
type: 'table',
datasource_name: 'table1',
@@ -304,7 +300,7 @@ describe('AdhocFilterEditPopoverSimpleTabContent', () => {
});
});
it('will display boolean operators when column is an expression', () => {
- const { props } = setup({
+ const props = setup({
datasource: {
type: 'table',
datasource_name: 'table1',
@@ -325,7 +321,7 @@ describe('AdhocFilterEditPopoverSimpleTabContent', () => {
});
});
it('sets comparator to true when operator is IS_TRUE', () => {
- const { props } = setup();
+ const props = setup();
const { onOperatorChange } = useSimpleTabFilterProps(props);
onOperatorChange(Operators.IsTrue);
expect(props.onChange.calledOnce).toBe(true);
@@ -334,7 +330,7 @@ describe('AdhocFilterEditPopoverSimpleTabContent', () => {
expect(props.onChange.lastCall.args[0].comparator).toBe(true);
});
it('sets comparator to false when operator is IS_FALSE', () => {
- const { props } = setup();
+ const props = setup();
const { onOperatorChange } = useSimpleTabFilterProps(props);
onOperatorChange(Operators.IsFalse);
expect(props.onChange.calledOnce).toBe(true);
@@ -343,7 +339,7 @@ describe('AdhocFilterEditPopoverSimpleTabContent', () => {
expect(props.onChange.lastCall.args[0].comparator).toBe(false);
});
it('sets comparator to null when operator is IS_NULL or IS_NOT_NULL', () => {
- const { props } = setup();
+ const props = setup();
const { onOperatorChange } = useSimpleTabFilterProps(props);
[Operators.IsNull, Operators.IsNotNull].forEach(op => {
onOperatorChange(op);
diff --git a/superset-frontend/src/explore/components/controls/MetricControl/MetricsControl.test.jsx b/superset-frontend/src/explore/components/controls/MetricControl/MetricsControl.test.jsx
index bfc7349107a2c..dd2c3e9e10436 100644
--- a/superset-frontend/src/explore/components/controls/MetricControl/MetricsControl.test.jsx
+++ b/superset-frontend/src/explore/components/controls/MetricControl/MetricsControl.test.jsx
@@ -16,16 +16,14 @@
* specific language governing permissions and limitations
* under the License.
*/
-import sinon from 'sinon';
-import { shallow } from 'enzyme';
-import { AGGREGATES } from 'src/explore/constants';
-import { LabelsContainer } from 'src/explore/components/controls/OptionControls';
-import { supersetTheme } from '@superset-ui/core';
+import { screen, render, selectOption } from 'spec/helpers/testing-library';
+import userEvent from '@testing-library/user-event';
import MetricsControl from 'src/explore/components/controls/MetricControl/MetricsControl';
import AdhocMetric, {
EXPRESSION_TYPES,
} from 'src/explore/components/controls/MetricControl/AdhocMetric';
+import { AGGREGATES } from 'src/explore/constants';
const defaultProps = {
name: 'metrics',
@@ -45,131 +43,140 @@ const defaultProps = {
};
function setup(overrides) {
- const onChange = sinon.spy();
+ const onChange = jest.fn();
const props = {
onChange,
- theme: supersetTheme,
...defaultProps,
...overrides,
};
- const wrapper = shallow();
- const component = wrapper.shallow();
- return { wrapper, component, onChange };
+ render(, { useDnd: true });
+ return { onChange };
}
-const valueColumn = { type: 'DOUBLE', column_name: 'value' };
+const valueColumn = { type: 'double', column_name: 'value' };
const sumValueAdhocMetric = new AdhocMetric({
+ expressionType: EXPRESSION_TYPES.SIMPLE,
column: valueColumn,
aggregate: AGGREGATES.SUM,
label: 'SUM(value)',
});
-// TODO: rewrite the tests to RTL
-describe.skip('MetricsControl', () => {
- it('renders Select', () => {
- const { component } = setup();
- expect(component.find(LabelsContainer)).toExist();
- });
+test('renders the LabelsContainer', () => {
+ setup();
+ expect(screen.getByText('Metrics')).toBeInTheDocument();
+});
- describe('constructor', () => {
- it('coerces Adhoc Metrics from form data into instances of the AdhocMetric class and leaves saved metrics', () => {
- const { component } = setup({
- value: [
- {
- expressionType: EXPRESSION_TYPES.SIMPLE,
- column: { type: 'double', column_name: 'value' },
- aggregate: AGGREGATES.SUM,
- label: 'SUM(value)',
- optionName: 'blahblahblah',
- },
- ],
- });
-
- const adhocMetric = component.state('value')[0];
- expect(adhocMetric instanceof AdhocMetric).toBe(true);
- expect(adhocMetric.optionName.length).toBeGreaterThan(10);
- expect(component.state('value')).toEqual([
- {
- expressionType: EXPRESSION_TYPES.SIMPLE,
- column: { type: 'double', column_name: 'value' },
- aggregate: AGGREGATES.SUM,
- label: 'SUM(value)',
- hasCustomLabel: false,
- optionName: 'blahblahblah',
- sqlExpression: null,
- },
- ]);
- });
+test('coerces Adhoc Metrics from form data into instances of the AdhocMetric class and leaves saved metrics', () => {
+ setup({
+ value: [sumValueAdhocMetric],
});
- describe('onChange', () => {
- it('handles creating a new metric', () => {
- const { component, onChange } = setup();
- component.instance().onNewMetric({
- metric_name: 'sum__value',
- expression: 'SUM(energy_usage.value)',
- });
- expect(onChange.lastCall.args).toEqual([['sum__value']]);
- });
+ const adhocMetric = screen.getByText('SUM(value)');
+ expect(adhocMetric).toBeInTheDocument();
+});
+
+test('handles creating a new metric', async () => {
+ const { onChange } = setup();
+
+ userEvent.click(screen.getByText(/add metric/i));
+ await selectOption('sum__value', /select saved metrics/i);
+ userEvent.click(screen.getByRole('button', { name: /save/i }));
+ expect(onChange).toHaveBeenCalledWith(['sum__value']);
+});
+
+test('accepts an edited metric from an AdhocMetricEditPopover', async () => {
+ const { onChange } = setup({
+ value: [sumValueAdhocMetric],
});
- describe('onMetricEdit', () => {
- it('accepts an edited metric from an AdhocMetricEditPopover', () => {
- const { component, onChange } = setup({
- value: [sumValueAdhocMetric],
- });
+ const metricLabel = screen.getByText('SUM(value)');
+ userEvent.click(metricLabel);
- const editedMetric = sumValueAdhocMetric.duplicateWith({
- aggregate: AGGREGATES.AVG,
- });
- component.instance().onMetricEdit(editedMetric, sumValueAdhocMetric);
+ await screen.findByText('aggregate');
+ selectOption('AVG', /select aggregate options/i);
- expect(onChange.lastCall.args).toEqual([[editedMetric]]);
- });
+ await screen.findByText('AVG(value)');
+
+ userEvent.click(screen.getByRole('button', { name: /save/i }));
+
+ expect(onChange).toHaveBeenCalledWith([
+ expect.objectContaining({
+ aggregate: AGGREGATES.AVG,
+ label: 'AVG(value)',
+ }),
+ ]);
+});
+
+test('removes metrics if savedMetrics changes', async () => {
+ setup({
+ value: [sumValueAdhocMetric],
});
- describe('option filter', () => {
- it('Removes metrics if savedMetrics changes', () => {
- const { props, component, onChange } = setup({
- value: [
- {
- expressionType: EXPRESSION_TYPES.SIMPLE,
- column: { type: 'double', column_name: 'value' },
- aggregate: AGGREGATES.SUM,
- label: 'SUM(value)',
- optionName: 'blahblahblah',
- },
- ],
- });
- expect(component.state('value')).toHaveLength(1);
-
- component.setProps({ ...props, columns: [] });
- expect(onChange.lastCall.args).toEqual([[]]);
- });
-
- it('Does not remove custom sql metric if savedMetrics changes', () => {
- const { props, component, onChange } = setup({
- value: [
- {
- expressionType: EXPRESSION_TYPES.SQL,
- sqlExpression: 'COUNT(*)',
- label: 'old label',
- hasCustomLabel: true,
- },
- ],
- });
- expect(component.state('value')).toHaveLength(1);
-
- component.setProps({ ...props, columns: [] });
- expect(onChange.calledOnce).toEqual(false);
- });
- it('Does not fail if no columns or savedMetrics are passed', () => {
- const { component } = setup({
- savedMetrics: null,
- columns: null,
- });
- expect(component.exists('.metrics-select')).toEqual(true);
- });
+ expect(screen.getByText('SUM(value)')).toBeInTheDocument();
+ userEvent.click(screen.getByText('SUM(value)'));
+
+ const savedTab = screen.getByRole('tab', { name: /saved/i });
+ userEvent.click(savedTab);
+ await selectOption('avg__value', /select saved metrics/i);
+
+ const simpleTab = screen.getByRole('tab', { name: /simple/i });
+ userEvent.click(simpleTab);
+ await screen.findByText('aggregate');
+
+ expect(screen.queryByText('SUM')).not.toBeInTheDocument();
+ expect(screen.queryByText('value')).not.toBeInTheDocument();
+});
+
+test('does not remove custom SQL metric if savedMetrics changes', async () => {
+ const { rerender } = render(
+ ,
+ { useDnd: true },
+ );
+
+ expect(screen.getByText('old label')).toBeInTheDocument();
+
+ // Simulate removing columns
+ rerender(
+ ,
+ );
+
+ expect(screen.getByText('old label')).toBeInTheDocument();
+});
+
+test('does not fail if no columns or savedMetrics are passed', () => {
+ setup({
+ savedMetrics: null,
+ columns: null,
});
+ expect(screen.getByText(/add metric/i)).toBeInTheDocument();
});
diff --git a/superset-frontend/src/explore/components/controls/SelectControl.jsx b/superset-frontend/src/explore/components/controls/SelectControl.jsx
index e0f1ae5f04461..b50d54c9adc76 100644
--- a/superset-frontend/src/explore/components/controls/SelectControl.jsx
+++ b/superset-frontend/src/explore/components/controls/SelectControl.jsx
@@ -87,6 +87,39 @@ const defaultProps = {
valueKey: 'value',
};
+export const innerGetOptions = props => {
+ const { choices, optionRenderer, valueKey } = props;
+ let options = [];
+ if (props.options) {
+ options = props.options.map(o => ({
+ ...o,
+ value: o[valueKey],
+ label: o.label || o[valueKey],
+ customLabel: optionRenderer ? optionRenderer(o) : undefined,
+ }));
+ } else if (choices) {
+ // Accepts different formats of input
+ options = choices.map(c => {
+ if (Array.isArray(c)) {
+ const [value, label] = c.length > 1 ? c : [c[0], c[0]];
+ return {
+ value,
+ label,
+ };
+ }
+ if (Object.is(c)) {
+ return {
+ ...c,
+ value: c[valueKey],
+ label: c.label || c[valueKey],
+ };
+ }
+ return { value: c, label: c };
+ });
+ }
+ return options;
+};
+
export default class SelectControl extends PureComponent {
constructor(props) {
super(props);
@@ -127,36 +160,7 @@ export default class SelectControl extends PureComponent {
}
getOptions(props) {
- const { choices, optionRenderer, valueKey } = props;
- let options = [];
- if (props.options) {
- options = props.options.map(o => ({
- ...o,
- value: o[valueKey],
- label: o.label || o[valueKey],
- customLabel: optionRenderer ? optionRenderer(o) : undefined,
- }));
- } else if (choices) {
- // Accepts different formats of input
- options = choices.map(c => {
- if (Array.isArray(c)) {
- const [value, label] = c.length > 1 ? c : [c[0], c[0]];
- return {
- value,
- label,
- };
- }
- if (Object.is(c)) {
- return {
- ...c,
- value: c[valueKey],
- label: c.label || c[valueKey],
- };
- }
- return { value: c, label: c };
- });
- }
- return options;
+ return innerGetOptions(props);
}
handleFilterOptions(text, option) {
diff --git a/superset-frontend/src/explore/components/controls/SelectControl.test.jsx b/superset-frontend/src/explore/components/controls/SelectControl.test.jsx
index d86c334be17b5..d90d549971918 100644
--- a/superset-frontend/src/explore/components/controls/SelectControl.test.jsx
+++ b/superset-frontend/src/explore/components/controls/SelectControl.test.jsx
@@ -16,11 +16,18 @@
* specific language governing permissions and limitations
* under the License.
*/
-import sinon from 'sinon';
-import { shallow } from 'enzyme';
-import { Select as SelectComponent } from 'src/components';
-import SelectControl from 'src/explore/components/controls/SelectControl';
-import { styledMount as mount } from 'spec/helpers/theming';
+import {
+ act,
+ createEvent,
+ fireEvent,
+ render,
+ screen,
+ within,
+} from 'spec/helpers/testing-library';
+import SelectControl, {
+ innerGetOptions,
+} from 'src/explore/components/controls/SelectControl';
+import userEvent from '@testing-library/user-event';
const defaultProps = {
choices: [
@@ -30,139 +37,202 @@ const defaultProps = {
],
name: 'row_limit',
label: 'Row Limit',
- valueKey: 'value', // shallow isn't passing SelectControl.defaultProps.valueKey through
- onChange: sinon.spy(),
+ valueKey: 'value',
+ onChange: jest.fn(),
};
+beforeEach(() => {
+ jest.useFakeTimers();
+});
+
+afterEach(() => {
+ jest.useRealTimers();
+});
+
const options = [
{ value: '1 year ago', label: '1 year ago' },
{ value: '1 week ago', label: '1 week ago' },
{ value: 'today', label: 'today' },
];
-describe('SelectControl', () => {
- let wrapper;
-
- beforeEach(() => {
- wrapper = shallow();
- });
+const renderSelectControl = (props = {}) => {
+ const overrideProps = {
+ ...defaultProps,
+ ...props,
+ };
+ const { container } = render();
+ return container;
+};
- it('calls props.onChange when select', () => {
- const select = wrapper.instance();
- select.onChange(50);
- expect(defaultProps.onChange.calledWith(50)).toBe(true);
+describe('SelectControl', () => {
+ it('calls props.onChange when select', async () => {
+ renderSelectControl();
+ defaultProps.onChange(50);
+ expect(defaultProps.onChange).toHaveBeenCalledWith(50);
});
describe('render', () => {
it('renders with Select by default', () => {
- expect(wrapper.find(SelectComponent)).toExist();
+ renderSelectControl();
+ const selectorWrapper = screen.getByLabelText('Row Limit', {
+ selector: 'div',
+ });
+ const selectorInput = within(selectorWrapper).getByLabelText(
+ 'Row Limit',
+ { selector: 'input' },
+ );
+ expect(selectorWrapper).toBeInTheDocument();
+ expect(selectorInput).toBeInTheDocument();
});
it('renders as mode multiple', () => {
- wrapper.setProps({ multi: true });
- expect(wrapper.find(SelectComponent)).toExist();
- expect(wrapper.find(SelectComponent).prop('mode')).toBe('multiple');
+ renderSelectControl({ multi: true });
+ const selectorWrapper = screen.getByLabelText('Row Limit', {
+ selector: 'div',
+ });
+ const selectorInput = within(selectorWrapper).getByLabelText(
+ 'Row Limit',
+ { selector: 'input' },
+ );
+ expect(selectorWrapper).toBeInTheDocument();
+ expect(selectorInput).toBeInTheDocument();
+ userEvent.click(selectorInput);
+ expect(screen.getByText('Select All (3)')).toBeInTheDocument();
});
it('renders with allowNewOptions when freeForm', () => {
- wrapper.setProps({ freeForm: true });
- expect(wrapper.find(SelectComponent)).toExist();
- expect(wrapper.find(SelectComponent).prop('allowNewOptions')).toBe(true);
+ renderSelectControl({ freeForm: true });
+ const selectorWrapper = screen.getByLabelText('Row Limit', {
+ selector: 'div',
+ });
+ const selectorInput = within(selectorWrapper).getByLabelText(
+ 'Row Limit',
+ { selector: 'input' },
+ );
+ expect(selectorWrapper).toBeInTheDocument();
+ expect(selectorInput).toBeInTheDocument();
+
+ // Expect a new option to be selectable.
+ userEvent.click(selectorInput);
+ userEvent.type(selectorInput, 'a new option');
+ act(() => jest.runAllTimers());
+ expect(within(selectorWrapper).getByRole('option')).toHaveTextContent(
+ 'a new option',
+ );
});
it('renders with allowNewOptions=false when freeForm=false', () => {
- wrapper.setProps({ freeForm: false });
- expect(wrapper.find(SelectComponent)).toExist();
- expect(wrapper.find(SelectComponent).prop('allowNewOptions')).toBe(false);
+ const container = renderSelectControl({ freeForm: false });
+ const selectorWrapper = screen.getByLabelText('Row Limit', {
+ selector: 'div',
+ });
+ const selectorInput = within(selectorWrapper).getByLabelText(
+ 'Row Limit',
+ { selector: 'input' },
+ );
+ expect(selectorWrapper).toBeInTheDocument();
+ expect(selectorInput).toBeInTheDocument();
+
+ // Expect no new option to be selectable.
+ userEvent.click(selectorInput);
+ userEvent.type(selectorInput, 'a new option');
+ act(() => jest.advanceTimersByTime(300));
+
+ expect(
+ container.querySelector('[role="option"]'),
+ ).not.toBeInTheDocument();
+ expect(within(selectorWrapper).getByText('No Data')).toBeInTheDocument();
});
it('renders with tokenSeparators', () => {
- wrapper.setProps({ tokenSeparators: ['\n', '\t', ';'] });
- expect(wrapper.find(SelectComponent)).toExist();
- expect(wrapper.find(SelectComponent).prop('tokenSeparators')).toEqual(
- expect.arrayContaining([expect.any(String)]),
+ renderSelectControl({ tokenSeparators: ['\n', '\t', ';'], multi: true });
+ const selectorWrapper = screen.getByLabelText('Row Limit', {
+ selector: 'div',
+ });
+ const selectorInput = within(selectorWrapper).getByLabelText(
+ 'Row Limit',
+ { selector: 'input' },
);
+ expect(selectorWrapper).toBeInTheDocument();
+ expect(selectorInput).toBeInTheDocument();
+
+ userEvent.click(selectorInput);
+ const paste = createEvent.paste(selectorInput, {
+ clipboardData: {
+ getData: () => '1 year ago;1 week ago',
+ },
+ });
+ fireEvent(selectorInput, paste);
+ const yearOption = screen.getByLabelText('1 year ago');
+ expect(yearOption).toBeInTheDocument();
+ expect(yearOption).toHaveAttribute('aria-selected', 'true');
+ const weekOption = screen.getByText(/1 week ago/, {
+ selector: 'div',
+ }).parentNode;
+ expect(weekOption?.getAttribute('aria-selected')).toEqual('true');
});
describe('empty placeholder', () => {
describe('withMulti', () => {
it('does not show a placeholder if there are no choices', () => {
- const withMulti = mount(
- ,
- );
- expect(withMulti.html()).not.toContain('option(s');
+ renderSelectControl({
+ choices: [],
+ multi: true,
+ placeholder: 'add something',
+ });
+ expect(screen.queryByRole('option')).not.toBeInTheDocument();
});
});
describe('withSingleChoice', () => {
- it('does not show a placeholder if there are no choices', () => {
- const singleChoice = mount(
- ,
- );
- expect(singleChoice.html()).not.toContain('option(s');
- });
- });
- describe('default placeholder', () => {
- it('does not show a placeholder if there are no options', () => {
- const defaultPlaceholder = mount(
- ,
- );
- expect(defaultPlaceholder.html()).not.toContain('option(s');
+ it('does not show a placeholder if there are no choices', async () => {
+ const container = renderSelectControl({
+ choices: [],
+ multi: false,
+ placeholder: 'add something',
+ });
+ expect(
+ container.querySelector('[role="option"]'),
+ ).not.toBeInTheDocument();
});
});
describe('all choices selected', () => {
it('does not show a placeholder', () => {
- const allChoicesSelected = mount(
- ,
- );
- expect(allChoicesSelected.html()).not.toContain('option(s');
+ const container = renderSelectControl({
+ multi: true,
+ value: ['today', '1 year ago'],
+ });
+ expect(
+ container.querySelector('[role="option"]'),
+ ).not.toBeInTheDocument();
+ expect(screen.queryByText('Select ...')).not.toBeInTheDocument();
});
});
});
describe('when select is multi', () => {
it('does not render the placeholder when a selection has been made', () => {
- wrapper = mount(
- ,
- );
- expect(wrapper.html()).not.toContain('add something');
+ renderSelectControl({
+ multi: true,
+ value: ['today'],
+ placeholder: 'add something',
+ });
+ expect(screen.queryByText('add something')).not.toBeInTheDocument();
});
});
describe('when select is single', () => {
it('does not render the placeholder when a selection has been made', () => {
- wrapper = mount(
- ,
- );
- expect(wrapper.html()).not.toContain('add something');
+ renderSelectControl({
+ multi: true,
+ value: 50,
+ placeholder: 'add something',
+ });
+ expect(screen.queryByText('add something')).not.toBeInTheDocument();
});
});
});
describe('getOptions', () => {
it('returns the correct options', () => {
- wrapper.setProps(defaultProps);
- expect(wrapper.instance().getOptions(defaultProps)).toEqual(options);
+ expect(innerGetOptions(defaultProps)).toEqual(options);
});
});
});
diff --git a/superset-frontend/src/explore/components/controls/TextAreaControl.test.jsx b/superset-frontend/src/explore/components/controls/TextAreaControl.test.jsx
index 402c69934d71f..dff4d3ef54a89 100644
--- a/superset-frontend/src/explore/components/controls/TextAreaControl.test.jsx
+++ b/superset-frontend/src/explore/components/controls/TextAreaControl.test.jsx
@@ -16,48 +16,48 @@
* specific language governing permissions and limitations
* under the License.
*/
-import sinon from 'sinon';
-import { styledMount as mount } from 'spec/helpers/theming';
-import { TextAreaEditor } from 'src/components/AsyncAceEditor';
-import { TextArea } from 'src/components/Input';
+import {
+ fireEvent,
+ render,
+ screen,
+ waitFor,
+} from 'spec/helpers/testing-library';
import TextAreaControl from 'src/explore/components/controls/TextAreaControl';
const defaultProps = {
name: 'x_axis_label',
label: 'X Axis Label',
- onChange: sinon.spy(),
+ onChange: jest.fn(),
};
describe('TextArea', () => {
- let wrapper;
- beforeEach(() => {
- wrapper = mount();
- });
-
it('renders a FormControl', () => {
- expect(wrapper.find(TextArea)).toExist();
+ render();
+ expect(screen.getByRole('textbox')).toBeVisible();
});
it('calls onChange when toggled', () => {
- const select = wrapper.find(TextArea);
- select.simulate('change', { target: { value: 'x' } });
- expect(defaultProps.onChange.calledWith('x')).toBe(true);
+ render();
+ const textArea = screen.getByRole('textbox');
+ fireEvent.change(textArea, { target: { value: 'x' } });
+ expect(defaultProps.onChange).toHaveBeenCalledWith('x');
});
- it('renders a AceEditor when language is specified', () => {
- const props = { ...defaultProps };
- props.language = 'markdown';
- wrapper = mount();
- expect(wrapper.find(TextArea)).not.toExist();
- expect(wrapper.find(TextAreaEditor)).toExist();
+ it('renders a AceEditor when language is specified', async () => {
+ const props = { ...defaultProps, language: 'markdown' };
+ const { container } = render();
+ expect(screen.queryByRole('textbox')).not.toBeInTheDocument();
+ await waitFor(() => {
+ expect(container.querySelector('.ace_text-input')).toBeInTheDocument();
+ });
});
it('calls onAreaEditorChange when entering in the AceEditor', () => {
- const props = { ...defaultProps };
- props.language = 'markdown';
- wrapper = mount();
- wrapper.simulate('change', { target: { value: 'x' } });
- expect(defaultProps.onChange.calledWith('x')).toBe(true);
+ const props = { ...defaultProps, language: 'markdown' };
+ render();
+ const textArea = screen.getByRole('textbox');
+ fireEvent.change(textArea, { target: { value: 'x' } });
+ expect(defaultProps.onChange).toHaveBeenCalledWith('x');
});
});
diff --git a/superset-frontend/src/explore/controlUtils/controlUtils.test.tsx b/superset-frontend/src/explore/controlUtils/controlUtils.test.tsx
index c18873460a922..228f6ac139806 100644
--- a/superset-frontend/src/explore/controlUtils/controlUtils.test.tsx
+++ b/superset-frontend/src/explore/controlUtils/controlUtils.test.tsx
@@ -156,7 +156,7 @@ describe('controlUtils', () => {
expect(control?.value).toBeNull();
});
- it('returns null for non-existent field', () => {
+ it('returns null for nonexistent field', () => {
const control = getControlState('NON_EXISTENT', 'table', state);
expect(control).toBeNull();
});
diff --git a/superset-frontend/src/explore/controlUtils/getControlValuesCompatibleWithDatasource.test.ts b/superset-frontend/src/explore/controlUtils/getControlValuesCompatibleWithDatasource.test.ts
index f0eb399267b11..df1afd69a0a0c 100644
--- a/superset-frontend/src/explore/controlUtils/getControlValuesCompatibleWithDatasource.test.ts
+++ b/superset-frontend/src/explore/controlUtils/getControlValuesCompatibleWithDatasource.test.ts
@@ -286,3 +286,175 @@ test('SQL ad-hoc filter values', () => {
sqlExpression: 'select * from sample_column_1;',
});
});
+
+test('no controlState value but valid column in datasource', () => {
+ const controlState = {
+ ...sharedControls.columns,
+ options: [], // no options in the control state
+ };
+
+ expect(
+ getValues({
+ ...controlState,
+ value: 'sample_column_1', // column only available in datasource
+ }),
+ ).toEqual('sample_column_1');
+
+ expect(
+ getValues({
+ ...controlState,
+ value: 'non_existing_column',
+ }),
+ ).toEqual(controlState.default);
+});
+
+test('no controlState value but valid saved metric in datasource', () => {
+ const controlState = {
+ ...sharedControls.metrics,
+ savedMetrics: [], // no saved metrics in the control state
+ };
+
+ expect(
+ getValues({
+ ...controlState,
+ value: 'saved_metric_2', // metric only available in datasource
+ }),
+ ).toEqual('saved_metric_2');
+
+ expect(
+ getValues({
+ ...controlState,
+ value: 'non_existing_metric',
+ }),
+ ).toEqual(controlState.default);
+});
+
+test('no controlState value but valid adhoc metric in datasource', () => {
+ const controlState = {
+ ...sharedControls.metrics,
+ columns: [], // no columns in control state
+ };
+
+ expect(
+ getValues({
+ ...controlState,
+ value: {
+ expressionType: 'SIMPLE',
+ column: { column_name: 'sample_column_1' }, // only in datasource
+ },
+ }),
+ ).toEqual({
+ expressionType: 'SIMPLE',
+ column: { column_name: 'sample_column_1' },
+ });
+
+ expect(
+ getValues({
+ ...controlState,
+ value: {
+ expressionType: 'SIMPLE',
+ column: { column_name: 'non_existing_column' },
+ },
+ }),
+ ).toEqual(controlState.default);
+});
+
+test('no controlState value but valid adhoc filter in datasource', () => {
+ const controlState = {
+ ...sharedControls.adhoc_filters,
+ columns: [], // no columns in control state
+ };
+
+ expect(
+ getValues({
+ ...controlState,
+ value: {
+ expressionType: 'SIMPLE',
+ subject: 'sample_column_1', // column available in datasource
+ },
+ }),
+ ).toEqual({
+ expressionType: 'SIMPLE',
+ subject: 'sample_column_1',
+ });
+
+ expect(
+ getValues({
+ ...controlState,
+ value: {
+ expressionType: 'SIMPLE',
+ subject: 'non_existing_column',
+ },
+ }),
+ ).toEqual(controlState.default);
+});
+
+test('SQL ad-hoc metric values without controlState columns', () => {
+ const controlState = {
+ ...sharedControls.metrics,
+ columns: [], // No columns in controlState
+ };
+
+ expect(
+ getValues({
+ ...controlState,
+ value: {
+ expressionType: 'SQL',
+ sqlExpression: 'SELECT COUNT(*) FROM sample_table;',
+ },
+ }),
+ ).toEqual({
+ datasourceWarning: true,
+ expressionType: 'SQL',
+ sqlExpression: 'SELECT COUNT(*) FROM sample_table;',
+ });
+
+ expect(
+ getValues({
+ ...controlState,
+ value: {
+ expressionType: 'SQL',
+ sqlExpression: 'SELECT column FROM non_existing_table;',
+ },
+ }),
+ ).toEqual({
+ datasourceWarning: true,
+ expressionType: 'SQL',
+ sqlExpression: 'SELECT column FROM non_existing_table;',
+ });
+});
+
+test('SQL ad-hoc filter values without controlState columns', () => {
+ const controlState = {
+ ...sharedControls.adhoc_filters,
+ columns: [], // No columns in controlState
+ };
+
+ expect(
+ getValues({
+ ...controlState,
+ value: {
+ expressionType: 'SQL',
+ sqlExpression: 'SELECT * FROM sample_table WHERE column = 1;',
+ },
+ }),
+ ).toEqual({
+ datasourceWarning: true,
+ expressionType: 'SQL',
+ sqlExpression: 'SELECT * FROM sample_table WHERE column = 1;',
+ });
+
+ expect(
+ getValues({
+ ...controlState,
+ value: {
+ expressionType: 'SQL',
+ sqlExpression: 'SELECT * FROM non_existing_table;',
+ },
+ }),
+ ).toEqual({
+ datasourceWarning: true,
+ expressionType: 'SQL',
+ sqlExpression: 'SELECT * FROM non_existing_table;',
+ });
+});
diff --git a/superset-frontend/src/explore/controlUtils/getControlValuesCompatibleWithDatasource.ts b/superset-frontend/src/explore/controlUtils/getControlValuesCompatibleWithDatasource.ts
index 7013b59764cb0..bfc53dc6d8257 100644
--- a/superset-frontend/src/explore/controlUtils/getControlValuesCompatibleWithDatasource.ts
+++ b/superset-frontend/src/explore/controlUtils/getControlValuesCompatibleWithDatasource.ts
@@ -27,6 +27,7 @@ import {
JsonValue,
SimpleAdhocFilter,
} from '@superset-ui/core';
+import { isEmpty } from 'lodash';
import AdhocMetric from 'src/explore/components/controls/MetricControl/AdhocMetric';
const isControlValueCompatibleWithDatasource = (
@@ -34,24 +35,32 @@ const isControlValueCompatibleWithDatasource = (
controlState: ControlState,
value: any,
) => {
+ // A datasource might have been deleted, in which case we can't validate
+ // only using the control state since it might have been hydrated with
+ // the wrong options or columns (empty arrays).
if (controlState.options && typeof value === 'string') {
if (
- controlState.options.some(
- (option: [string | number, string] | { column_name: string }) =>
- Array.isArray(option)
- ? option[0] === value
- : option.column_name === value,
- )
+ (!isEmpty(controlState.options) &&
+ controlState.options.some(
+ (option: [string | number, string] | { column_name: string }) =>
+ Array.isArray(option)
+ ? option[0] === value
+ : option.column_name === value,
+ )) ||
+ !isEmpty(datasource?.columns)
) {
- return datasource.columns.some(column => column.column_name === value);
+ return datasource.columns.some(
+ (column: Column) => column.column_name === value,
+ );
}
}
if (
controlState.savedMetrics &&
isSavedMetric(value) &&
- controlState.savedMetrics.some(
+ (controlState.savedMetrics.some(
(savedMetric: Metric) => savedMetric.metric_name === value,
- )
+ ) ||
+ !isEmpty(datasource?.metrics))
) {
return datasource.metrics.some(
(metric: Metric) => metric.metric_name === value,
@@ -60,11 +69,13 @@ const isControlValueCompatibleWithDatasource = (
if (
controlState.columns &&
(isAdhocMetricSimple(value) || isSimpleAdhocFilter(value)) &&
- controlState.columns.some(
- (column: Column) =>
- column.column_name === (value as AdhocMetric).column?.column_name ||
- column.column_name === (value as SimpleAdhocFilter).subject,
- )
+ ((!isEmpty(controlState.columns) &&
+ controlState.columns.some(
+ (column: Column) =>
+ column.column_name === (value as AdhocMetric).column?.column_name ||
+ column.column_name === (value as SimpleAdhocFilter).subject,
+ )) ||
+ !isEmpty(datasource?.columns))
) {
return datasource.columns.some(
(column: Column) =>
diff --git a/superset-frontend/src/explore/controlUtils/standardizedFormData.ts b/superset-frontend/src/explore/controlUtils/standardizedFormData.ts
index 082ff6c91daa2..b8d7ad7c7955e 100644
--- a/superset-frontend/src/explore/controlUtils/standardizedFormData.ts
+++ b/superset-frontend/src/explore/controlUtils/standardizedFormData.ts
@@ -193,7 +193,7 @@ export class StandardizedFormData {
controlsState: ControlStateMapping;
} {
/*
- * Transfrom form_data between different viz. Return new form_data and controlsState.
+ * Transform form_data between different viz. Return new form_data and controlsState.
* 1. get memorized form_data by viz type or get previous form_data
* 2. collect public control values
* 3. generate initial targetControlsState
diff --git a/superset-frontend/src/features/databases/DatabaseModal/ExtraOptions.tsx b/superset-frontend/src/features/databases/DatabaseModal/ExtraOptions.tsx
index 6cf1deaac0389..e4de2df222301 100644
--- a/superset-frontend/src/features/databases/DatabaseModal/ExtraOptions.tsx
+++ b/superset-frontend/src/features/databases/DatabaseModal/ExtraOptions.tsx
@@ -172,11 +172,11 @@ const ExtraOptions = ({
indeterminate={false}
checked={!!db?.allow_dml}
onChange={onInputChange}
- labelText={t('Allow DML')}
+ labelText={t('Allow DDL and DML')}
/>
diff --git a/superset-frontend/src/features/databases/DatabaseModal/index.test.tsx b/superset-frontend/src/features/databases/DatabaseModal/index.test.tsx
index 7150b863abd50..49b80fc96112e 100644
--- a/superset-frontend/src/features/databases/DatabaseModal/index.test.tsx
+++ b/superset-frontend/src/features/databases/DatabaseModal/index.test.tsx
@@ -700,9 +700,9 @@ describe('DatabaseModal', () => {
/force all tables and views to be created in this schema when clicking ctas or cvas in sql lab\./i,
);
const allowDMLCheckbox = screen.getByRole('checkbox', {
- name: /allow dml/i,
+ name: /allow ddl and dml/i,
});
- const allowDMLText = screen.getByText(/allow dml/i);
+ const allowDMLText = screen.getByText(/allow ddl and dml/i);
const enableQueryCostEstimationCheckbox = screen.getByRole('checkbox', {
name: /enable query cost estimation/i,
});
diff --git a/superset-frontend/src/features/datasets/AddDataset/DatasetPanel/types.ts b/superset-frontend/src/features/datasets/AddDataset/DatasetPanel/types.ts
index c2330f3f10a48..264662047d26c 100644
--- a/superset-frontend/src/features/datasets/AddDataset/DatasetPanel/types.ts
+++ b/superset-frontend/src/features/datasets/AddDataset/DatasetPanel/types.ts
@@ -63,8 +63,8 @@ export interface IDatabaseTable {
}
/**
- * Checks if a given item matches the isIDatabsetTable interface
- * @param item Object to check if it matches the isIDatabsetTable interface
+ * Checks if a given item matches the isIDatabaseTable interface
+ * @param item Object to check if it matches the isIDatabaseTable interface
* @returns boolean true if matches interface
*/
export const isIDatabaseTable = (item: any): boolean => {
diff --git a/superset-frontend/src/features/datasets/AddDataset/LeftPanel/LeftPanel.test.tsx b/superset-frontend/src/features/datasets/AddDataset/LeftPanel/LeftPanel.test.tsx
index 88836d2c25d52..4f8055cffea25 100644
--- a/superset-frontend/src/features/datasets/AddDataset/LeftPanel/LeftPanel.test.tsx
+++ b/superset-frontend/src/features/datasets/AddDataset/LeftPanel/LeftPanel.test.tsx
@@ -35,7 +35,7 @@ beforeEach(() => {
allow_file_upload: 'Allow Csv Upload',
allow_ctas: 'Allow Ctas',
allow_cvas: 'Allow Cvas',
- allow_dml: 'Allow Dml',
+ allow_dml: 'Allow DDL and DML',
allow_multi_schema_metadata_fetch: 'Allow Multi Schema Metadata Fetch',
allow_run_async: 'Allow Run Async',
allows_cost_estimate: 'Allows Cost Estimate',
@@ -271,7 +271,7 @@ test('searches for a table name', async () => {
});
});
-test('renders a warning icon when a table name has a pre-existing dataset', async () => {
+test('renders a warning icon when a table name has a preexisting dataset', async () => {
render(
{
);
useEffect(() => {
- // Todo: this useEffect should be used to call all count methods conncurently
+ // Todo: this useEffect should be used to call all count methods concurrently
// when we populate data for the new tabs. For right separating out this
// api call for building the usage page.
if (id) {
diff --git a/superset-frontend/src/features/home/RightMenu.test.tsx b/superset-frontend/src/features/home/RightMenu.test.tsx
index 50c0ca3259c2b..ba50a5256c372 100644
--- a/superset-frontend/src/features/home/RightMenu.test.tsx
+++ b/superset-frontend/src/features/home/RightMenu.test.tsx
@@ -308,10 +308,13 @@ test('If there is a DB with allow_file_upload set as True the option should be e
userEvent.hover(dropdown);
const dataMenu = await screen.findByText(dropdownItems[0].label);
userEvent.hover(dataMenu);
- expect(await screen.findByText('Upload CSV to database')).toBeInTheDocument();
+ const csvMenu = await screen.findByText('Upload CSV to database');
+ expect(csvMenu).toBeInTheDocument();
expect(
await screen.findByText('Upload Excel to database'),
).toBeInTheDocument();
+
+ expect(csvMenu).not.toHaveAttribute('aria-disabled', 'true');
});
test('If there is NOT a DB with allow_file_upload set as True the option should be disabled', async () => {
@@ -341,10 +344,11 @@ test('If there is NOT a DB with allow_file_upload set as True the option should
userEvent.hover(dropdown);
const dataMenu = await screen.findByText(dropdownItems[0].label);
userEvent.hover(dataMenu);
- expect(await screen.findByText('Upload CSV to database')).toBeInTheDocument();
- expect(
- (await screen.findByText('Upload CSV to database')).closest('a'),
- ).not.toBeInTheDocument();
+ const csvMenu = await screen.findByRole('menuitem', {
+ name: 'Upload CSV to database',
+ });
+ expect(csvMenu).toBeInTheDocument();
+ expect(csvMenu).toHaveAttribute('aria-disabled', 'true');
});
test('Logs out and clears local storage item redux', async () => {
diff --git a/superset-frontend/src/features/home/RightMenu.tsx b/superset-frontend/src/features/home/RightMenu.tsx
index 99a139836684e..e5c34fdd9e497 100644
--- a/superset-frontend/src/features/home/RightMenu.tsx
+++ b/superset-frontend/src/features/home/RightMenu.tsx
@@ -247,7 +247,7 @@ const RightMenu = ({
SupersetClient.get({
endpoint: `/api/v1/database/?q=${rison.encode(payload)}`,
}).then(({ json }: Record) => {
- // There might be some existings Gsheets and Clickhouse DBs
+ // There might be some existing Gsheets and Clickhouse DBs
// with allow_file_upload set as True which is not possible from now on
const allowedDatabasesWithFileUpload =
json?.result?.filter(
@@ -313,7 +313,7 @@ const RightMenu = ({
const buildMenuItem = (item: MenuObjectChildProps) =>
item.disable ? (
-
+
{item.label}
diff --git a/superset-frontend/src/features/queries/QueryPreviewModal.test.tsx b/superset-frontend/src/features/queries/QueryPreviewModal.test.tsx
index 336b9dbb3c2ba..54cfd45ae2741 100644
--- a/superset-frontend/src/features/queries/QueryPreviewModal.test.tsx
+++ b/superset-frontend/src/features/queries/QueryPreviewModal.test.tsx
@@ -82,7 +82,7 @@ describe('QueryPreviewModal', () => {
await waitForComponentToPaint(wrapper);
});
- it('renders a SynxHighlighter', () => {
+ it('renders a SyntaxHighlighter', () => {
expect(wrapper.find(SyntaxHighlighter)).toExist();
});
diff --git a/superset-frontend/src/features/rls/RowLevelSecurityModal.tsx b/superset-frontend/src/features/rls/RowLevelSecurityModal.tsx
index 31ef7993aac66..2b3ef730c938d 100644
--- a/superset-frontend/src/features/rls/RowLevelSecurityModal.tsx
+++ b/superset-frontend/src/features/rls/RowLevelSecurityModal.tsx
@@ -119,7 +119,7 @@ export interface RowLevelSecurityModalProps {
show: boolean;
}
-const DEAFULT_RULE = {
+const DEFAULT_RULE = {
name: '',
filter_type: FilterType.Regular,
tables: [],
@@ -133,7 +133,7 @@ function RowLevelSecurityModal(props: RowLevelSecurityModalProps) {
const { rule, addDangerToast, addSuccessToast, onHide, show } = props;
const [currentRule, setCurrentRule] = useState({
- ...DEAFULT_RULE,
+ ...DEFAULT_RULE,
});
const [disableSave, setDisableSave] = useState(true);
@@ -204,7 +204,7 @@ function RowLevelSecurityModal(props: RowLevelSecurityModalProps) {
// initialize
useEffect(() => {
if (!isEditMode) {
- setCurrentRule({ ...DEAFULT_RULE });
+ setCurrentRule({ ...DEFAULT_RULE });
} else if (rule?.id !== null && !loading && !fetchError) {
fetchResource(rule.id as number);
}
@@ -249,7 +249,7 @@ function RowLevelSecurityModal(props: RowLevelSecurityModalProps) {
const hide = () => {
clearError();
- setCurrentRule({ ...DEAFULT_RULE });
+ setCurrentRule({ ...DEFAULT_RULE });
onHide();
};
diff --git a/superset-frontend/src/hooks/apiResources/apiResources.ts b/superset-frontend/src/hooks/apiResources/apiResources.ts
index 722998c32fb08..04166db1d32f2 100644
--- a/superset-frontend/src/hooks/apiResources/apiResources.ts
+++ b/superset-frontend/src/hooks/apiResources/apiResources.ts
@@ -33,8 +33,8 @@ export enum ResourceStatus {
export type Resource = LoadingState | CompleteState | ErrorState;
// Trying out something a little different: a separate type per status.
-// This should let Typescript know whether a Resource has a result or error.
-// It's possible that I'm expecting too much from Typescript here.
+// This should let TypeScript know whether a Resource has a result or error.
+// It's possible that I'm expecting too much from TypeScript here.
// If this ends up causing problems, we can change the type to:
//
// export type Resource = {
diff --git a/superset-frontend/src/hooks/apiResources/tables.ts b/superset-frontend/src/hooks/apiResources/tables.ts
index 86b080745fc14..e32113babccfe 100644
--- a/superset-frontend/src/hooks/apiResources/tables.ts
+++ b/superset-frontend/src/hooks/apiResources/tables.ts
@@ -85,7 +85,7 @@ export type TableMetaData = {
columns: Column[];
};
-type TableMetadataReponse = {
+type TableMetadataResponse = {
json: TableMetaData;
response: Response;
};
@@ -130,7 +130,7 @@ const tableApi = api.injectEndpoints({
catalog,
schema,
})}`,
- transformResponse: ({ json }: TableMetadataReponse) => json,
+ transformResponse: ({ json }: TableMetadataResponse) => json,
}),
}),
tableExtendedMetadata: builder.query<
diff --git a/superset-frontend/src/pages/Chart/index.tsx b/superset-frontend/src/pages/Chart/index.tsx
index e8e9ae127fe5a..af9386279f027 100644
--- a/superset-frontend/src/pages/Chart/index.tsx
+++ b/superset-frontend/src/pages/Chart/index.tsx
@@ -43,7 +43,10 @@ import { getItem, LocalStorageKeys } from 'src/utils/localStorageHelpers';
import { getFormDataWithDashboardContext } from 'src/explore/controlUtils/getFormDataWithDashboardContext';
const isValidResult = (rv: JsonObject): boolean =>
- rv?.result?.form_data && isDefined(rv?.result?.dataset?.id);
+ rv?.result?.form_data && rv?.result?.dataset;
+
+const hasDatasetId = (rv: JsonObject): boolean =>
+ isDefined(rv?.result?.dataset?.id);
const fetchExploreData = async (exploreUrlParams: URLSearchParams) => {
try {
@@ -52,7 +55,19 @@ const fetchExploreData = async (exploreUrlParams: URLSearchParams) => {
endpoint: 'api/v1/explore/',
})(exploreUrlParams);
if (isValidResult(rv)) {
- return rv;
+ if (hasDatasetId(rv)) {
+ return rv;
+ }
+ // Since there's no dataset id but the API responded with a valid payload,
+ // we assume the dataset was deleted, so we preserve some values from previous
+ // state so if the user decide to swap the datasource, the chart config remains
+ fallbackExploreInitialData.form_data = {
+ ...rv.result.form_data,
+ ...fallbackExploreInitialData.form_data,
+ };
+ if (rv.result?.slice) {
+ fallbackExploreInitialData.slice = rv.result.slice;
+ }
}
let message = t('Failed to load chart data');
const responseError = rv?.result?.message;
diff --git a/superset-frontend/src/pages/DatabaseList/DatabaseList.test.jsx b/superset-frontend/src/pages/DatabaseList/DatabaseList.test.jsx
index 8027ddaecd52f..b15126b3457ef 100644
--- a/superset-frontend/src/pages/DatabaseList/DatabaseList.test.jsx
+++ b/superset-frontend/src/pages/DatabaseList/DatabaseList.test.jsx
@@ -164,14 +164,7 @@ describe('Admin DatabaseList', () => {
});
await waitForComponentToPaint(wrapper);
- expect(wrapper.find(DeleteModal).props().description)
- .toMatchInlineSnapshot(`
-
-
- The database db 0 is linked to 0 charts that appear on 0 dashboards and users have 0 SQL Lab tabs using this database open. Are you sure you want to continue? Deleting the database will break those objects.
-
+ The database
+
+
+ db 0
+
+
+ is linked to 0 charts that appear on 0 dashboards and users have 0 SQL Lab tabs using this database open. Are you sure you want to continue? Deleting the database will break those objects.
+
+
+`;
diff --git a/superset-frontend/src/pages/DatabaseList/index.tsx b/superset-frontend/src/pages/DatabaseList/index.tsx
index 1d29cbc567b5f..776dbbe817aa3 100644
--- a/superset-frontend/src/pages/DatabaseList/index.tsx
+++ b/superset-frontend/src/pages/DatabaseList/index.tsx
@@ -27,7 +27,6 @@ import rison from 'rison';
import { useSelector } from 'react-redux';
import { useQueryParams, BooleanParam } from 'use-query-params';
import { LocalStorageKeys, setItem } from 'src/utils/localStorageHelpers';
-
import Loading from 'src/components/Loading';
import { useListViewResource } from 'src/views/CRUD/hooks';
import {
@@ -65,8 +64,8 @@ const dbConfigExtraExtension = extensionsRegistry.get(
const PAGE_SIZE = 25;
interface DatabaseDeleteObject extends DatabaseObject {
- chart_count: number;
- dashboard_count: number;
+ charts: any;
+ dashboards: any;
sqllab_tab_count: number;
}
interface DatabaseListProps {
@@ -170,8 +169,8 @@ function DatabaseList({
.then(({ json = {} }) => {
setDatabaseCurrentlyDeleting({
...database,
- chart_count: json.charts.count,
- dashboard_count: json.dashboards.count,
+ charts: json.charts,
+ dashboards: json.dashboards,
sqllab_tab_count: json.sqllab_tab_states.count,
});
})
@@ -281,7 +280,7 @@ function DatabaseList({
SupersetClient.get({
endpoint: `/api/v1/database/?q=${rison.encode(payload)}`,
}).then(({ json }: Record) => {
- // There might be some existings Gsheets and Clickhouse DBs
+ // There might be some existing Gsheets and Clickhouse DBs
// with allow_file_upload set as True which is not possible from now on
const allowedDatabasesWithFileUpload =
json?.result?.filter(
@@ -609,14 +608,82 @@ function DatabaseList({
description={
<>
+ {t('The database')}{' '}
+ {databaseCurrentlyDeleting.database_name}{' '}
{t(
- 'The database %s is linked to %s charts that appear on %s dashboards and users have %s SQL Lab tabs using this database open. Are you sure you want to continue? Deleting the database will break those objects.',
- databaseCurrentlyDeleting.database_name,
- databaseCurrentlyDeleting.chart_count,
- databaseCurrentlyDeleting.dashboard_count,
+ 'is linked to %s charts that appear on %s dashboards and users have %s SQL Lab tabs using this database open. Are you sure you want to continue? Deleting the database will break those objects.',
+ databaseCurrentlyDeleting.charts.count,
+ databaseCurrentlyDeleting.dashboards.count,
databaseCurrentlyDeleting.sqllab_tab_count,
)}
+ {t('The dataset')}
+ {datasetCurrentlyDeleting.table_name}
{t(
- 'The dataset %s is linked to %s charts that appear on %s dashboards. Are you sure you want to continue? Deleting the dataset will break those objects.',
- datasetCurrentlyDeleting.table_name,
- datasetCurrentlyDeleting.chart_count,
- datasetCurrentlyDeleting.dashboard_count,
+ 'is linked to %s charts that appear on %s dashboards. Are you sure you want to continue? Deleting the dataset will break those objects.',
+ datasetCurrentlyDeleting.charts.count,
+ datasetCurrentlyDeleting.dashboards.count,
)}
+ >
+ )}
{DatasetDeleteRelatedExtension && (
{
expect(table).toBeInTheDocument();
const nameColumn = await within(table).findByText('Name');
- const fitlerTypeColumn = await within(table).findByText('Filter Type');
+ const filterTypeColumn = await within(table).findByText('Filter Type');
const groupKeyColumn = await within(table).findByText('Group Key');
const clauseColumn = await within(table).findByText('Clause');
const modifiedColumn = await within(table).findByText('Last modified');
const actionsColumn = await within(table).findByText('Actions');
expect(nameColumn).toBeInTheDocument();
- expect(fitlerTypeColumn).toBeInTheDocument();
+ expect(filterTypeColumn).toBeInTheDocument();
expect(groupKeyColumn).toBeInTheDocument();
expect(clauseColumn).toBeInTheDocument();
expect(modifiedColumn).toBeInTheDocument();
diff --git a/superset-frontend/src/setup/setupApp.ts b/superset-frontend/src/setup/setupApp.ts
index 0f31024cdaec8..44826829a0d9e 100644
--- a/superset-frontend/src/setup/setupApp.ts
+++ b/superset-frontend/src/setup/setupApp.ts
@@ -95,6 +95,6 @@ export default function setupApp() {
window.jQuery = $;
require('bootstrap');
- // setup appwide custom error messages
+ // set up app wide custom error messages
setupErrorMessages();
}
diff --git a/superset-frontend/src/types/Owner.ts b/superset-frontend/src/types/Owner.ts
index 91e9d29c9bf94..b8c0f4962cba8 100644
--- a/superset-frontend/src/types/Owner.ts
+++ b/superset-frontend/src/types/Owner.ts
@@ -22,7 +22,8 @@
*/
export default interface Owner {
- first_name: string;
+ first_name?: string;
id: number;
- last_name: string;
+ last_name?: string;
+ full_name?: string;
}
diff --git a/superset-frontend/src/utils/getOwnerName.test.ts b/superset-frontend/src/utils/getOwnerName.test.ts
index a4a25e57b24ed..27ec7e99b9446 100644
--- a/superset-frontend/src/utils/getOwnerName.test.ts
+++ b/superset-frontend/src/utils/getOwnerName.test.ts
@@ -22,6 +22,8 @@ test('render owner name correctly', () => {
expect(getOwnerName({ id: 1, first_name: 'Foo', last_name: 'Bar' })).toEqual(
'Foo Bar',
);
+
+ expect(getOwnerName({ id: 2, full_name: 'John Doe' })).toEqual('John Doe');
});
test('return empty string for undefined owner', () => {
diff --git a/superset-frontend/src/utils/getOwnerName.ts b/superset-frontend/src/utils/getOwnerName.ts
index 2534c45f2cbb1..42c1519c8671b 100644
--- a/superset-frontend/src/utils/getOwnerName.ts
+++ b/superset-frontend/src/utils/getOwnerName.ts
@@ -22,5 +22,5 @@ export default function getOwnerName(owner?: Owner): string {
if (!owner) {
return '';
}
- return `${owner.first_name} ${owner.last_name}`;
+ return owner.full_name || `${owner.first_name} ${owner.last_name}`;
}
diff --git a/superset-frontend/src/views/routes.test.tsx b/superset-frontend/src/views/routes.test.tsx
index b808ad0e76a53..b4c29e08b2019 100644
--- a/superset-frontend/src/views/routes.test.tsx
+++ b/superset-frontend/src/views/routes.test.tsx
@@ -28,6 +28,6 @@ describe('isFrontendRoute', () => {
});
it('returns false if a route does not match', () => {
- expect(isFrontendRoute('/non-existent/path/')).toBe(false);
+ expect(isFrontendRoute('/nonexistent/path/')).toBe(false);
});
});
diff --git a/superset-frontend/src/views/store.ts b/superset-frontend/src/views/store.ts
index ebed3f67fc9e3..9fcdf53e9d295 100644
--- a/superset-frontend/src/views/store.ts
+++ b/superset-frontend/src/views/store.ts
@@ -141,7 +141,7 @@ const reducers = {
explore,
};
-/* In some cases the jinja template injects two seperate React apps into basic.html
+/* In some cases the jinja template injects two separate React apps into basic.html
* One for the top navigation Menu and one for the application below the Menu
* The first app to connect to the Redux debugger wins which is the menu blocking
* the application from being able to connect to the redux debugger.
diff --git a/superset-frontend/tools/eslint-plugin-translation-vars/index.js b/superset-frontend/tools/eslint-plugin-translation-vars/index.js
index 69493f3b9e39d..9f3a54c42a313 100644
--- a/superset-frontend/tools/eslint-plugin-translation-vars/index.js
+++ b/superset-frontend/tools/eslint-plugin-translation-vars/index.js
@@ -41,7 +41,7 @@ module.exports = {
context.report({
node,
message:
- "Don't use variables in translation string templates. Flask-babel is a static translation translation service, so it can’t handle strings that include variables",
+ "Don't use variables in translation string templates. Flask-babel is a static translation service, so it can’t handle strings that include variables",
});
}
}
diff --git a/superset-frontend/webpack.config.js b/superset-frontend/webpack.config.js
index 9c972702b240d..10bf4d49eec1d 100644
--- a/superset-frontend/webpack.config.js
+++ b/superset-frontend/webpack.config.js
@@ -45,10 +45,10 @@ const ROOT_DIR = path.resolve(__dirname, '..');
const TRANSLATIONS_DIR = path.resolve(__dirname, '../superset/translations');
const getAvailableTranslationCodes = () => {
- const LOCALE_CODE_MAPPING = {
- zh: 'zh-cn',
- };
- try {
+ if (process.env.BUILD_TRANSLATIONS === 'true') {
+ const LOCALE_CODE_MAPPING = {
+ zh: 'zh-cn',
+ };
const files = fs.readdirSync(TRANSLATIONS_DIR);
return files
.filter(file =>
@@ -57,10 +57,9 @@ const getAvailableTranslationCodes = () => {
.filter(dirName => !dirName.startsWith('__'))
.map(dirName => dirName.replace('_', '-'))
.map(dirName => LOCALE_CODE_MAPPING[dirName] || dirName);
- } catch (err) {
- console.error('Error reading the directory:', err);
- return [];
}
+ // Indicates to the MomentLocalesPlugin that we only want to keep 'en'.
+ return [];
};
const {
diff --git a/superset-websocket/.eslintignore b/superset-websocket/.eslintignore
deleted file mode 100644
index 8e5e2bc3dbce3..0000000000000
--- a/superset-websocket/.eslintignore
+++ /dev/null
@@ -1,20 +0,0 @@
-#
-# 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.
-#
-*.min.js
-node_modules
-dist
-coverage
diff --git a/superset-websocket/.eslintrc.js b/superset-websocket/.eslintrc.js
deleted file mode 100644
index c60fd24d67861..0000000000000
--- a/superset-websocket/.eslintrc.js
+++ /dev/null
@@ -1,40 +0,0 @@
-/**
- * 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.
- */
-module.exports = {
- root: true,
- parser: '@typescript-eslint/parser',
- env: {
- node: true,
- browser: true,
- },
- plugins: [
- '@typescript-eslint',
- 'lodash',
- ],
- extends: [
- 'eslint:recommended',
- 'plugin:@typescript-eslint/recommended',
- 'prettier',
- ],
- rules: {
- "lodash/import-scope": [2, "member"],
- "@typescript-eslint/explicit-module-boundary-types": 0,
- "@typescript-eslint/no-var-requires": 0,
- },
-};
diff --git a/superset-websocket/eslint.config.js b/superset-websocket/eslint.config.js
new file mode 100644
index 0000000000000..8249fe0e84e31
--- /dev/null
+++ b/superset-websocket/eslint.config.js
@@ -0,0 +1,53 @@
+/**
+ * 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.
+ */
+
+const typescriptEslintParser = require('@typescript-eslint/parser');
+const typescriptEslintPlugin = require('@typescript-eslint/eslint-plugin');
+const typescriptEslint = require('typescript-eslint');
+const lodashEslintPlugin = require('eslint-plugin-lodash');
+const eslintConfigPrettier = require('eslint-config-prettier');
+const js = require('@eslint/js');
+const globals = require('globals');
+
+module.exports = [
+ js.configs.recommended,
+ ...typescriptEslint.configs.recommended,
+ eslintConfigPrettier,
+ {
+ files: ['**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx'],
+ ignores: ['*.min.js', 'node_modules', 'dist', 'coverage'],
+ languageOptions: {
+ parser: typescriptEslintParser,
+ globals: {
+ ...globals.browser,
+ ...globals.node,
+ },
+ },
+ plugins: {
+ typescript: typescriptEslintPlugin,
+ lodash: lodashEslintPlugin,
+ },
+ rules: {
+ 'lodash/import-scope': [2, 'member'],
+ '@typescript-eslint/explicit-module-boundary-types': 0,
+ '@typescript-eslint/no-var-requires': 0,
+ '@typescript-eslint/no-require-imports': 0, // Re-enable once superset-websocket is converted to ESM
+ },
+ },
+];
diff --git a/superset-websocket/package-lock.json b/superset-websocket/package-lock.json
index 7a88389215281..606c91600addb 100644
--- a/superset-websocket/package-lock.json
+++ b/superset-websocket/package-lock.json
@@ -9,7 +9,6 @@
"version": "0.0.1",
"license": "Apache-2.0",
"dependencies": {
- "@types/lodash": "^4.17.7",
"cookie": "^0.6.0",
"hot-shots": "^10.0.0",
"ioredis": "^4.28.0",
@@ -20,24 +19,28 @@
"ws": "^8.18.0"
},
"devDependencies": {
+ "@eslint/js": "^9.11.0",
"@types/cookie": "^0.6.0",
+ "@types/eslint__js": "^8.42.3",
"@types/ioredis": "^4.27.8",
"@types/jest": "^29.5.12",
"@types/jsonwebtoken": "^9.0.6",
"@types/lodash": "^4.17.7",
- "@types/node": "^22.0.2",
+ "@types/node": "^22.7.4",
"@types/uuid": "^10.0.0",
"@types/ws": "^8.5.12",
- "@typescript-eslint/eslint-plugin": "^5.62.0",
- "@typescript-eslint/parser": "^5.62.0",
- "eslint": "^8.57.0",
+ "@typescript-eslint/eslint-plugin": "^8.8.0",
+ "@typescript-eslint/parser": "^8.6.0",
+ "eslint": "^9.11.0",
"eslint-config-prettier": "^9.1.0",
- "eslint-plugin-lodash": "^7.4.0",
+ "eslint-plugin-lodash": "^8.0.0",
+ "globals": "^15.9.0",
"jest": "^29.7.0",
"prettier": "^3.3.3",
"ts-jest": "^29.2.5",
"ts-node": "^10.9.2",
- "typescript": "^5.5.4"
+ "typescript": "^5.5.4",
+ "typescript-eslint": "^8.8.0"
},
"engines": {
"node": "^16.9.1",
@@ -657,6 +660,16 @@
"node": ">=6.9.0"
}
},
+ "node_modules/@babel/traverse/node_modules/globals": {
+ "version": "11.12.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
+ "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/@babel/types": {
"version": "7.24.5",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.5.tgz",
@@ -708,10 +721,11 @@
}
},
"node_modules/@eslint-community/eslint-utils": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.3.0.tgz",
- "integrity": "sha512-v3oplH6FYCULtFuCeqyuTd9D2WKO937Dxdq+GmHOLL72TTRriLxz2VLlNfkZRsvj6PKnOPAtuT6dwrs/pA5DvA==",
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
+ "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"eslint-visitor-keys": "^3.3.0"
},
@@ -723,24 +737,41 @@
}
},
"node_modules/@eslint-community/regexpp": {
- "version": "4.6.2",
- "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.6.2.tgz",
- "integrity": "sha512-pPTNuaAG3QMH+buKyBIGJs3g/S5y0caxw0ygM3YyE6yJFySwiGGSzA+mM3KJ8QQvzeLh3blwgSonkFjgQdxzMw==",
+ "version": "4.11.1",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.1.tgz",
+ "integrity": "sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": "^12.0.0 || ^14.0.0 || >=16.0.0"
}
},
+ "node_modules/@eslint/config-array": {
+ "version": "0.18.0",
+ "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.18.0.tgz",
+ "integrity": "sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/object-schema": "^2.1.4",
+ "debug": "^4.3.1",
+ "minimatch": "^3.1.2"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
"node_modules/@eslint/eslintrc": {
- "version": "2.1.4",
- "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz",
- "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==",
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz",
+ "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"ajv": "^6.12.4",
"debug": "^4.3.2",
- "espree": "^9.6.0",
- "globals": "^13.19.0",
+ "espree": "^10.0.1",
+ "globals": "^14.0.0",
"ignore": "^5.2.0",
"import-fresh": "^3.2.1",
"js-yaml": "^4.1.0",
@@ -748,7 +779,7 @@
"strip-json-comments": "^3.1.1"
},
"engines": {
- "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
@@ -758,18 +789,17 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
- "dev": true
+ "dev": true,
+ "license": "Python-2.0"
},
"node_modules/@eslint/eslintrc/node_modules/globals": {
- "version": "13.23.0",
- "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz",
- "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==",
+ "version": "14.0.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
+ "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
"dev": true,
- "dependencies": {
- "type-fest": "^0.20.2"
- },
+ "license": "MIT",
"engines": {
- "node": ">=8"
+ "node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
@@ -780,6 +810,7 @@
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"argparse": "^2.0.1"
},
@@ -788,26 +819,36 @@
}
},
"node_modules/@eslint/js": {
- "version": "8.57.0",
- "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz",
- "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==",
+ "version": "9.11.0",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.11.0.tgz",
+ "integrity": "sha512-LPkkenkDqyzTFauZLLAPhIb48fj6drrfMvRGSL9tS3AcZBSVTllemLSNyCvHNNL2t797S/6DJNSIwRwXgMO/eQ==",
"dev": true,
+ "license": "MIT",
"engines": {
- "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
- "node_modules/@humanwhocodes/config-array": {
- "version": "0.11.14",
- "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
- "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==",
+ "node_modules/@eslint/object-schema": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz",
+ "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/plugin-kit": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.0.tgz",
+ "integrity": "sha512-vH9PiIMMwvhCx31Af3HiGzsVNULDbyVkHXwlemn/B0TFj/00ho3y55efXrUZTfQipxoHC5u4xq6zblww1zm1Ig==",
"dev": true,
+ "license": "Apache-2.0",
"dependencies": {
- "@humanwhocodes/object-schema": "^2.0.2",
- "debug": "^4.3.1",
- "minimatch": "^3.0.5"
+ "levn": "^0.4.1"
},
"engines": {
- "node": ">=10.10.0"
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/@humanwhocodes/module-importer": {
@@ -823,11 +864,19 @@
"url": "https://github.com/sponsors/nzakas"
}
},
- "node_modules/@humanwhocodes/object-schema": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz",
- "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==",
- "dev": true
+ "node_modules/@humanwhocodes/retry": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.0.tgz",
+ "integrity": "sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
},
"node_modules/@istanbuljs/load-nyc-config": {
"version": "1.1.0",
@@ -1615,6 +1664,34 @@
"integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==",
"dev": true
},
+ "node_modules/@types/eslint": {
+ "version": "9.6.1",
+ "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz",
+ "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "*",
+ "@types/json-schema": "*"
+ }
+ },
+ "node_modules/@types/eslint__js": {
+ "version": "8.42.3",
+ "resolved": "https://registry.npmjs.org/@types/eslint__js/-/eslint__js-8.42.3.tgz",
+ "integrity": "sha512-alfG737uhmPdnvkrLdZLcEKJ/B8s9Y4hrZ+YAdzUeoArBlSUERA2E87ROfOaS4jd/C45fzOoZzidLc1IPwLqOw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/eslint": "*"
+ }
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
+ "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@types/graceful-fs": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz",
@@ -1671,7 +1748,8 @@
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
- "dev": true
+ "dev": true,
+ "license": "MIT"
},
"node_modules/@types/jsonwebtoken": {
"version": "9.0.6",
@@ -1689,20 +1767,14 @@
"dev": true
},
"node_modules/@types/node": {
- "version": "22.0.2",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-22.0.2.tgz",
- "integrity": "sha512-yPL6DyFwY5PiMVEwymNeqUTKsDczQBJ/5T7W/46RwLU/VH+AA8aT5TZkvBviLKLbbm0hlfftEkGrNzfRk/fofQ==",
+ "version": "22.7.4",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.4.tgz",
+ "integrity": "sha512-y+NPi1rFzDs1NdQHHToqeiX2TIS79SWEAw9GYhkkx8bD0ChpfqC+n2j5OXOCpzfojBEBt6DnEnnG9MY0zk1XLg==",
"dev": true,
"dependencies": {
- "undici-types": "~6.11.1"
+ "undici-types": "~6.19.2"
}
},
- "node_modules/@types/semver": {
- "version": "7.5.8",
- "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz",
- "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==",
- "dev": true
- },
"node_modules/@types/stack-utils": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz",
@@ -1745,32 +1817,31 @@
"dev": true
},
"node_modules/@typescript-eslint/eslint-plugin": {
- "version": "5.62.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz",
- "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==",
+ "version": "8.8.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.8.0.tgz",
+ "integrity": "sha512-wORFWjU30B2WJ/aXBfOm1LX9v9nyt9D3jsSOxC3cCaTQGCW5k4jNpmjFv3U7p/7s4yvdjHzwtv2Sd2dOyhjS0A==",
"dev": true,
"dependencies": {
- "@eslint-community/regexpp": "^4.4.0",
- "@typescript-eslint/scope-manager": "5.62.0",
- "@typescript-eslint/type-utils": "5.62.0",
- "@typescript-eslint/utils": "5.62.0",
- "debug": "^4.3.4",
+ "@eslint-community/regexpp": "^4.10.0",
+ "@typescript-eslint/scope-manager": "8.8.0",
+ "@typescript-eslint/type-utils": "8.8.0",
+ "@typescript-eslint/utils": "8.8.0",
+ "@typescript-eslint/visitor-keys": "8.8.0",
"graphemer": "^1.4.0",
- "ignore": "^5.2.0",
- "natural-compare-lite": "^1.4.0",
- "semver": "^7.3.7",
- "tsutils": "^3.21.0"
+ "ignore": "^5.3.1",
+ "natural-compare": "^1.4.0",
+ "ts-api-utils": "^1.3.0"
},
"engines": {
- "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
- "@typescript-eslint/parser": "^5.0.0",
- "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0",
+ "eslint": "^8.57.0 || ^9.0.0"
},
"peerDependenciesMeta": {
"typescript": {
@@ -1779,25 +1850,26 @@
}
},
"node_modules/@typescript-eslint/parser": {
- "version": "5.62.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz",
- "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==",
+ "version": "8.8.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.8.0.tgz",
+ "integrity": "sha512-uEFUsgR+tl8GmzmLjRqz+VrDv4eoaMqMXW7ruXfgThaAShO9JTciKpEsB+TvnfFfbg5IpujgMXVV36gOJRLtZg==",
"dev": true,
"dependencies": {
- "@typescript-eslint/scope-manager": "5.62.0",
- "@typescript-eslint/types": "5.62.0",
- "@typescript-eslint/typescript-estree": "5.62.0",
+ "@typescript-eslint/scope-manager": "8.8.0",
+ "@typescript-eslint/types": "8.8.0",
+ "@typescript-eslint/typescript-estree": "8.8.0",
+ "@typescript-eslint/visitor-keys": "8.8.0",
"debug": "^4.3.4"
},
"engines": {
- "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
- "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ "eslint": "^8.57.0 || ^9.0.0"
},
"peerDependenciesMeta": {
"typescript": {
@@ -1806,16 +1878,16 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
- "version": "5.62.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz",
- "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==",
+ "version": "8.8.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.8.0.tgz",
+ "integrity": "sha512-EL8eaGC6gx3jDd8GwEFEV091210U97J0jeEHrAYvIYosmEGet4wJ+g0SYmLu+oRiAwbSA5AVrt6DxLHfdd+bUg==",
"dev": true,
"dependencies": {
- "@typescript-eslint/types": "5.62.0",
- "@typescript-eslint/visitor-keys": "5.62.0"
+ "@typescript-eslint/types": "8.8.0",
+ "@typescript-eslint/visitor-keys": "8.8.0"
},
"engines": {
- "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
@@ -1823,26 +1895,23 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
- "version": "5.62.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz",
- "integrity": "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==",
+ "version": "8.8.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.8.0.tgz",
+ "integrity": "sha512-IKwJSS7bCqyCeG4NVGxnOP6lLT9Okc3Zj8hLO96bpMkJab+10HIfJbMouLrlpyOr3yrQ1cA413YPFiGd1mW9/Q==",
"dev": true,
"dependencies": {
- "@typescript-eslint/typescript-estree": "5.62.0",
- "@typescript-eslint/utils": "5.62.0",
+ "@typescript-eslint/typescript-estree": "8.8.0",
+ "@typescript-eslint/utils": "8.8.0",
"debug": "^4.3.4",
- "tsutils": "^3.21.0"
+ "ts-api-utils": "^1.3.0"
},
"engines": {
- "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
- "peerDependencies": {
- "eslint": "*"
- },
"peerDependenciesMeta": {
"typescript": {
"optional": true
@@ -1850,12 +1919,12 @@
}
},
"node_modules/@typescript-eslint/types": {
- "version": "5.62.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz",
- "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==",
+ "version": "8.8.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.8.0.tgz",
+ "integrity": "sha512-QJwc50hRCgBd/k12sTykOJbESe1RrzmX6COk8Y525C9l7oweZ+1lw9JiU56im7Amm8swlz00DRIlxMYLizr2Vw==",
"dev": true,
"engines": {
- "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
@@ -1863,21 +1932,22 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
- "version": "5.62.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz",
- "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==",
+ "version": "8.8.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.8.0.tgz",
+ "integrity": "sha512-ZaMJwc/0ckLz5DaAZ+pNLmHv8AMVGtfWxZe/x2JVEkD5LnmhWiQMMcYT7IY7gkdJuzJ9P14fRy28lUrlDSWYdw==",
"dev": true,
"dependencies": {
- "@typescript-eslint/types": "5.62.0",
- "@typescript-eslint/visitor-keys": "5.62.0",
+ "@typescript-eslint/types": "8.8.0",
+ "@typescript-eslint/visitor-keys": "8.8.0",
"debug": "^4.3.4",
- "globby": "^11.1.0",
+ "fast-glob": "^3.3.2",
"is-glob": "^4.0.3",
- "semver": "^7.3.7",
- "tsutils": "^3.21.0"
+ "minimatch": "^9.0.4",
+ "semver": "^7.6.0",
+ "ts-api-utils": "^1.3.0"
},
"engines": {
- "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
@@ -1889,61 +1959,75 @@
}
}
},
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+ "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+ "dev": true,
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
+ "version": "9.0.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
+ "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+ "dev": true,
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
"node_modules/@typescript-eslint/utils": {
- "version": "5.62.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz",
- "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==",
+ "version": "8.8.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.8.0.tgz",
+ "integrity": "sha512-QE2MgfOTem00qrlPgyByaCHay9yb1+9BjnMFnSFkUKQfu7adBXDTnCAivURnuPPAG/qiB+kzKkZKmKfaMT0zVg==",
"dev": true,
"dependencies": {
- "@eslint-community/eslint-utils": "^4.2.0",
- "@types/json-schema": "^7.0.9",
- "@types/semver": "^7.3.12",
- "@typescript-eslint/scope-manager": "5.62.0",
- "@typescript-eslint/types": "5.62.0",
- "@typescript-eslint/typescript-estree": "5.62.0",
- "eslint-scope": "^5.1.1",
- "semver": "^7.3.7"
+ "@eslint-community/eslint-utils": "^4.4.0",
+ "@typescript-eslint/scope-manager": "8.8.0",
+ "@typescript-eslint/types": "8.8.0",
+ "@typescript-eslint/typescript-estree": "8.8.0"
},
"engines": {
- "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
- "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ "eslint": "^8.57.0 || ^9.0.0"
}
},
"node_modules/@typescript-eslint/visitor-keys": {
- "version": "5.62.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz",
- "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==",
+ "version": "8.8.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.8.0.tgz",
+ "integrity": "sha512-8mq51Lx6Hpmd7HnA2fcHQo3YgfX1qbccxQOgZcb4tvasu//zXRaA1j5ZRFeCw/VRAdFi4mRM9DnZw0Nu0Q2d1g==",
"dev": true,
"dependencies": {
- "@typescript-eslint/types": "5.62.0",
- "eslint-visitor-keys": "^3.3.0"
+ "@typescript-eslint/types": "8.8.0",
+ "eslint-visitor-keys": "^3.4.3"
},
"engines": {
- "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
- "node_modules/@ungap/structured-clone": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz",
- "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==",
- "dev": true
- },
"node_modules/acorn": {
- "version": "7.4.1",
- "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
- "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==",
+ "version": "8.12.1",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz",
+ "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==",
"dev": true,
- "peer": true,
+ "license": "MIT",
"bin": {
"acorn": "bin/acorn"
},
@@ -1956,6 +2040,7 @@
"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
"integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
"dev": true,
+ "license": "MIT",
"peerDependencies": {
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
}
@@ -1965,6 +2050,7 @@
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
@@ -2052,15 +2138,6 @@
"sprintf-js": "~1.0.2"
}
},
- "node_modules/array-union": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
- "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
- "dev": true,
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/async": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.3.tgz",
@@ -2524,30 +2601,6 @@
"node": ">=0.3.1"
}
},
- "node_modules/dir-glob": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
- "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
- "dev": true,
- "dependencies": {
- "path-type": "^4.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/doctrine": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
- "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
- "dev": true,
- "dependencies": {
- "esutils": "^2.0.2"
- },
- "engines": {
- "node": ">=6.0.0"
- }
- },
"node_modules/ecdsa-sig-formatter": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
@@ -2628,43 +2681,40 @@
}
},
"node_modules/eslint": {
- "version": "8.57.0",
- "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz",
- "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==",
+ "version": "9.11.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.11.0.tgz",
+ "integrity": "sha512-yVS6XODx+tMFMDFcG4+Hlh+qG7RM6cCJXtQhCKLSsr3XkLvWggHjCqjfh0XsPPnt1c56oaT6PMgW9XWQQjdHXA==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
- "@eslint-community/regexpp": "^4.6.1",
- "@eslint/eslintrc": "^2.1.4",
- "@eslint/js": "8.57.0",
- "@humanwhocodes/config-array": "^0.11.14",
+ "@eslint-community/regexpp": "^4.11.0",
+ "@eslint/config-array": "^0.18.0",
+ "@eslint/eslintrc": "^3.1.0",
+ "@eslint/js": "9.11.0",
+ "@eslint/plugin-kit": "^0.2.0",
"@humanwhocodes/module-importer": "^1.0.1",
+ "@humanwhocodes/retry": "^0.3.0",
"@nodelib/fs.walk": "^1.2.8",
- "@ungap/structured-clone": "^1.2.0",
"ajv": "^6.12.4",
"chalk": "^4.0.0",
"cross-spawn": "^7.0.2",
"debug": "^4.3.2",
- "doctrine": "^3.0.0",
"escape-string-regexp": "^4.0.0",
- "eslint-scope": "^7.2.2",
- "eslint-visitor-keys": "^3.4.3",
- "espree": "^9.6.1",
- "esquery": "^1.4.2",
+ "eslint-scope": "^8.0.2",
+ "eslint-visitor-keys": "^4.0.0",
+ "espree": "^10.1.0",
+ "esquery": "^1.5.0",
"esutils": "^2.0.2",
"fast-deep-equal": "^3.1.3",
- "file-entry-cache": "^6.0.1",
+ "file-entry-cache": "^8.0.0",
"find-up": "^5.0.0",
"glob-parent": "^6.0.2",
- "globals": "^13.19.0",
- "graphemer": "^1.4.0",
"ignore": "^5.2.0",
"imurmurhash": "^0.1.4",
"is-glob": "^4.0.0",
"is-path-inside": "^3.0.3",
- "js-yaml": "^4.1.0",
"json-stable-stringify-without-jsonify": "^1.0.1",
- "levn": "^0.4.1",
"lodash.merge": "^4.6.2",
"minimatch": "^3.1.2",
"natural-compare": "^1.4.0",
@@ -2676,10 +2726,18 @@
"eslint": "bin/eslint.js"
},
"engines": {
- "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
- "url": "https://opencollective.com/eslint"
+ "url": "https://eslint.org/donate"
+ },
+ "peerDependencies": {
+ "jiti": "*"
+ },
+ "peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ }
}
},
"node_modules/eslint-config-prettier": {
@@ -2695,10 +2753,11 @@
}
},
"node_modules/eslint-plugin-lodash": {
- "version": "7.4.0",
- "resolved": "https://registry.npmjs.org/eslint-plugin-lodash/-/eslint-plugin-lodash-7.4.0.tgz",
- "integrity": "sha512-Tl83UwVXqe1OVeBRKUeWcfg6/pCW1GTRObbdnbEJgYwjxp5Q92MEWQaH9+dmzbRt6kvYU1Mp893E79nJiCSM8A==",
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-lodash/-/eslint-plugin-lodash-8.0.0.tgz",
+ "integrity": "sha512-7DA8485FolmWRzh+8t4S8Pzin2TTuWfb0ZW3j/2fYElgk82ZanFz8vDcvc4BBPceYdX1p/za+tkbO68maDBGGw==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"lodash": "^4.17.21"
},
@@ -2706,20 +2765,24 @@
"node": ">=10"
},
"peerDependencies": {
- "eslint": ">=2"
+ "eslint": ">=9.0.0"
}
},
"node_modules/eslint-scope": {
- "version": "5.1.1",
- "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
- "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==",
+ "version": "8.0.2",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.0.2.tgz",
+ "integrity": "sha512-6E4xmrTw5wtxnLA5wYL3WDfhZ/1bUBGOXV0zQvVRDOtrR8D0p6W7fs3JweNYhwRYeGvd/1CKX2se0/2s7Q/nJA==",
"dev": true,
+ "license": "BSD-2-Clause",
"dependencies": {
"esrecurse": "^4.3.0",
- "estraverse": "^4.1.1"
+ "estraverse": "^5.2.0"
},
"engines": {
- "node": ">=8.0.0"
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
}
},
"node_modules/eslint-visitor-keys": {
@@ -2734,12 +2797,6 @@
"url": "https://opencollective.com/eslint"
}
},
- "node_modules/eslint/node_modules/argparse": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
- "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
- "dev": true
- },
"node_modules/eslint/node_modules/escape-string-regexp": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
@@ -2752,31 +2809,19 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/eslint/node_modules/eslint-scope": {
- "version": "7.2.2",
- "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
- "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==",
+ "node_modules/eslint/node_modules/eslint-visitor-keys": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz",
+ "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==",
"dev": true,
- "dependencies": {
- "esrecurse": "^4.3.0",
- "estraverse": "^5.2.0"
- },
+ "license": "Apache-2.0",
"engines": {
- "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
- "node_modules/eslint/node_modules/estraverse": {
- "version": "5.3.0",
- "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
- "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
- "dev": true,
- "engines": {
- "node": ">=4.0"
- }
- },
"node_modules/eslint/node_modules/find-up": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
@@ -2805,46 +2850,6 @@
"node": ">=10.13.0"
}
},
- "node_modules/eslint/node_modules/globals": {
- "version": "13.19.0",
- "resolved": "https://registry.npmjs.org/globals/-/globals-13.19.0.tgz",
- "integrity": "sha512-dkQ957uSRWHw7CFXLUtUHQI3g3aWApYhfNR2O6jn/907riyTYKVBmxYVROkBcY614FSSeSJh7Xm7SrUWCxvJMQ==",
- "dev": true,
- "dependencies": {
- "type-fest": "^0.20.2"
- },
- "engines": {
- "node": ">=8"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/eslint/node_modules/js-yaml": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
- "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
- "dev": true,
- "dependencies": {
- "argparse": "^2.0.1"
- },
- "bin": {
- "js-yaml": "bin/js-yaml.js"
- }
- },
- "node_modules/eslint/node_modules/levn": {
- "version": "0.4.1",
- "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
- "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
- "dev": true,
- "dependencies": {
- "prelude-ls": "^1.2.1",
- "type-check": "~0.4.0"
- },
- "engines": {
- "node": ">= 0.8.0"
- }
- },
"node_modules/eslint/node_modules/locate-path": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
@@ -2907,54 +2912,35 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/eslint/node_modules/prelude-ls": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
- "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
- "dev": true,
- "engines": {
- "node": ">= 0.8.0"
- }
- },
- "node_modules/eslint/node_modules/type-check": {
- "version": "0.4.0",
- "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
- "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
- "dev": true,
- "dependencies": {
- "prelude-ls": "^1.2.1"
- },
- "engines": {
- "node": ">= 0.8.0"
- }
- },
"node_modules/espree": {
- "version": "9.6.1",
- "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz",
- "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==",
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-10.1.0.tgz",
+ "integrity": "sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==",
"dev": true,
+ "license": "BSD-2-Clause",
"dependencies": {
- "acorn": "^8.9.0",
+ "acorn": "^8.12.0",
"acorn-jsx": "^5.3.2",
- "eslint-visitor-keys": "^3.4.1"
+ "eslint-visitor-keys": "^4.0.0"
},
"engines": {
- "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
- "node_modules/espree/node_modules/acorn": {
- "version": "8.11.2",
- "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz",
- "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==",
+ "node_modules/espree/node_modules/eslint-visitor-keys": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz",
+ "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==",
"dev": true,
- "bin": {
- "acorn": "bin/acorn"
- },
+ "license": "Apache-2.0",
"engines": {
- "node": ">=0.4.0"
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
}
},
"node_modules/esprima": {
@@ -2971,10 +2957,11 @@
}
},
"node_modules/esquery": {
- "version": "1.4.2",
- "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.2.tgz",
- "integrity": "sha512-JVSoLdTlTDkmjFmab7H/9SL9qGSyjElT3myyKp7krqjVFQCDLmj1QFaCLRFBszBKI0XVZaiiXvuPIX3ZwHe1Ng==",
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz",
+ "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==",
"dev": true,
+ "license": "BSD-3-Clause",
"dependencies": {
"estraverse": "^5.1.0"
},
@@ -2982,20 +2969,12 @@
"node": ">=0.10"
}
},
- "node_modules/esquery/node_modules/estraverse": {
- "version": "5.3.0",
- "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
- "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
- "dev": true,
- "engines": {
- "node": ">=4.0"
- }
- },
"node_modules/esrecurse": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
"integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
"dev": true,
+ "license": "BSD-2-Clause",
"dependencies": {
"estraverse": "^5.2.0"
},
@@ -3003,20 +2982,12 @@
"node": ">=4.0"
}
},
- "node_modules/esrecurse/node_modules/estraverse": {
- "version": "5.2.0",
- "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz",
- "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==",
- "dev": true,
- "engines": {
- "node": ">=4.0"
- }
- },
"node_modules/estraverse": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
- "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
"dev": true,
+ "license": "BSD-2-Clause",
"engines": {
"node": ">=4.0"
}
@@ -3026,6 +2997,7 @@
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
"dev": true,
+ "license": "BSD-2-Clause",
"engines": {
"node": ">=0.10.0"
}
@@ -3091,13 +3063,15 @@
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
- "dev": true
+ "dev": true,
+ "license": "MIT"
},
"node_modules/fast-glob": {
- "version": "3.2.12",
- "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz",
- "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==",
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
+ "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"@nodelib/fs.stat": "^2.0.2",
"@nodelib/fs.walk": "^1.2.3",
@@ -3145,15 +3119,16 @@
"integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw=="
},
"node_modules/file-entry-cache": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
- "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==",
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
+ "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "flat-cache": "^3.0.4"
+ "flat-cache": "^4.0.0"
},
"engines": {
- "node": "^10.12.0 || >=12.0.0"
+ "node": ">=16.0.0"
}
},
"node_modules/file-uri-to-path": {
@@ -3218,23 +3193,25 @@
}
},
"node_modules/flat-cache": {
- "version": "3.0.4",
- "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz",
- "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==",
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
+ "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "flatted": "^3.1.0",
- "rimraf": "^3.0.2"
+ "flatted": "^3.2.9",
+ "keyv": "^4.5.4"
},
"engines": {
- "node": "^10.12.0 || >=12.0.0"
+ "node": ">=16"
}
},
"node_modules/flatted": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.1.1.tgz",
- "integrity": "sha512-zAoAQiudy+r5SvnSw3KJy5os/oRJYHzrzja/tBDqrZtNhUw8bt6y8OBzMWcjWr+8liV8Eb6yOhw8WZ7VFZ5ZzA==",
- "dev": true
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz",
+ "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==",
+ "dev": true,
+ "license": "ISC"
},
"node_modules/fn.name": {
"version": "1.1.0",
@@ -3331,6 +3308,7 @@
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"dev": true,
+ "license": "ISC",
"dependencies": {
"is-glob": "^4.0.1"
},
@@ -3339,29 +3317,13 @@
}
},
"node_modules/globals": {
- "version": "11.12.0",
- "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
- "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
- "dev": true,
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/globby": {
- "version": "11.1.0",
- "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz",
- "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==",
+ "version": "15.9.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-15.9.0.tgz",
+ "integrity": "sha512-SmSKyLLKFbSr6rptvP8izbyxJL4ILwqO9Jg23UA0sDlGlu58V59D1//I3vlc0KJphVdUR7vMjHIplYnzBxorQA==",
"dev": true,
- "dependencies": {
- "array-union": "^2.1.0",
- "dir-glob": "^3.0.1",
- "fast-glob": "^3.2.9",
- "ignore": "^5.2.0",
- "merge2": "^1.4.1",
- "slash": "^3.0.0"
- },
+ "license": "MIT",
"engines": {
- "node": ">=10"
+ "node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
@@ -3427,10 +3389,11 @@
}
},
"node_modules/ignore": {
- "version": "5.2.1",
- "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.1.tgz",
- "integrity": "sha512-d2qQLzTJ9WxQftPAuEQpSPmKqzxePjzVbpAVv62AQ64NTL+wR4JkrVqR/LqFsFEUsHDAiId52mJteHDFuDkElA==",
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">= 4"
}
@@ -3440,6 +3403,7 @@
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
"integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"parent-module": "^1.0.0",
"resolve-from": "^4.0.0"
@@ -3456,6 +3420,7 @@
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
"integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=4"
}
@@ -4950,6 +4915,13 @@
"node": ">=4"
}
},
+ "node_modules/json-buffer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/json-parse-even-better-errors": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
@@ -4960,7 +4932,8 @@
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
- "dev": true
+ "dev": true,
+ "license": "MIT"
},
"node_modules/json-stable-stringify-without-jsonify": {
"version": "1.0.1",
@@ -5020,6 +4993,16 @@
"safe-buffer": "^5.0.1"
}
},
+ "node_modules/keyv": {
+ "version": "4.5.4",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+ "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "json-buffer": "3.0.1"
+ }
+ },
"node_modules/kleur": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz",
@@ -5043,6 +5026,20 @@
"node": ">=6"
}
},
+ "node_modules/levn": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
"node_modules/lines-and-columns": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
@@ -5185,18 +5182,20 @@
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">= 8"
}
},
"node_modules/micromatch": {
- "version": "4.0.4",
- "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz",
- "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==",
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+ "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "braces": "^3.0.1",
- "picomatch": "^2.2.3"
+ "braces": "^3.0.3",
+ "picomatch": "^2.3.1"
},
"engines": {
"node": ">=8.6"
@@ -5240,12 +5239,6 @@
"integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=",
"dev": true
},
- "node_modules/natural-compare-lite": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz",
- "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==",
- "dev": true
- },
"node_modules/node-int64": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
@@ -5360,6 +5353,7 @@
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
"integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"callsites": "^3.0.0"
},
@@ -5418,15 +5412,6 @@
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"dev": true
},
- "node_modules/path-type": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
- "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
- "dev": true,
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/picocolors": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
@@ -5434,10 +5419,11 @@
"dev": true
},
"node_modules/picomatch": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz",
- "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==",
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=8.6"
},
@@ -5466,6 +5452,16 @@
"node": ">=8"
}
},
+ "node_modules/prelude-ls": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
"node_modules/prettier": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz",
@@ -5521,10 +5517,11 @@
}
},
"node_modules/punycode": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
- "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=6"
}
@@ -5674,18 +5671,6 @@
"node": ">=0.10.0"
}
},
- "node_modules/rimraf": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
- "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
- "dev": true,
- "dependencies": {
- "glob": "^7.1.3"
- },
- "bin": {
- "rimraf": "bin.js"
- }
- },
"node_modules/run-parallel": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
@@ -6018,6 +6003,19 @@
"node": ">= 14.0.0"
}
},
+ "node_modules/ts-api-utils": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz",
+ "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=16"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.2.0"
+ }
+ },
"node_modules/ts-jest": {
"version": "29.2.5",
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.5.tgz",
@@ -6109,18 +6107,6 @@
}
}
},
- "node_modules/ts-node/node_modules/acorn": {
- "version": "8.8.1",
- "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.1.tgz",
- "integrity": "sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==",
- "dev": true,
- "bin": {
- "acorn": "bin/acorn"
- },
- "engines": {
- "node": ">=0.4.0"
- }
- },
"node_modules/ts-node/node_modules/acorn-walk": {
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz",
@@ -6130,22 +6116,17 @@
"node": ">=0.4.0"
}
},
- "node_modules/tslib": {
- "version": "1.14.1",
- "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
- "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
- "dev": true
- },
- "node_modules/tsutils": {
- "version": "3.21.0",
- "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz",
- "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==",
+ "node_modules/type-check": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "tslib": "^1.8.1"
+ "prelude-ls": "^1.2.1"
},
"engines": {
- "node": ">= 6"
+ "node": ">= 0.8.0"
}
},
"node_modules/type-detect": {
@@ -6157,18 +6138,6 @@
"node": ">=4"
}
},
- "node_modules/type-fest": {
- "version": "0.20.2",
- "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
- "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
- "dev": true,
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/typescript": {
"version": "5.5.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz",
@@ -6182,10 +6151,33 @@
"node": ">=14.17"
}
},
+ "node_modules/typescript-eslint": {
+ "version": "8.8.0",
+ "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.8.0.tgz",
+ "integrity": "sha512-BjIT/VwJ8+0rVO01ZQ2ZVnjE1svFBiRczcpr1t1Yxt7sT25VSbPfrJtDsQ8uQTy2pilX5nI9gwxhUyLULNentw==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/eslint-plugin": "8.8.0",
+ "@typescript-eslint/parser": "8.8.0",
+ "@typescript-eslint/utils": "8.8.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
"node_modules/undici-types": {
- "version": "6.11.1",
- "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.11.1.tgz",
- "integrity": "sha512-mIDEX2ek50x0OlRgxryxsenE5XaQD4on5U2inY7RApK3SOJpofyw7uW2AyfMKkhAxXIceo2DeWGVGwyvng1GNQ==",
+ "version": "6.19.8",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
+ "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
"dev": true
},
"node_modules/unix-dgram": {
@@ -6237,6 +6229,7 @@
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
"integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
"dev": true,
+ "license": "BSD-2-Clause",
"dependencies": {
"punycode": "^2.1.0"
}
@@ -6924,6 +6917,14 @@
"@babel/types": "^7.24.5",
"debug": "^4.3.1",
"globals": "^11.1.0"
+ },
+ "dependencies": {
+ "globals": {
+ "version": "11.12.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
+ "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
+ "dev": true
+ }
}
},
"@babel/types": {
@@ -6968,30 +6969,41 @@
}
},
"@eslint-community/eslint-utils": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.3.0.tgz",
- "integrity": "sha512-v3oplH6FYCULtFuCeqyuTd9D2WKO937Dxdq+GmHOLL72TTRriLxz2VLlNfkZRsvj6PKnOPAtuT6dwrs/pA5DvA==",
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
+ "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==",
"dev": true,
"requires": {
"eslint-visitor-keys": "^3.3.0"
}
},
"@eslint-community/regexpp": {
- "version": "4.6.2",
- "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.6.2.tgz",
- "integrity": "sha512-pPTNuaAG3QMH+buKyBIGJs3g/S5y0caxw0ygM3YyE6yJFySwiGGSzA+mM3KJ8QQvzeLh3blwgSonkFjgQdxzMw==",
+ "version": "4.11.1",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.1.tgz",
+ "integrity": "sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q==",
"dev": true
},
+ "@eslint/config-array": {
+ "version": "0.18.0",
+ "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.18.0.tgz",
+ "integrity": "sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==",
+ "dev": true,
+ "requires": {
+ "@eslint/object-schema": "^2.1.4",
+ "debug": "^4.3.1",
+ "minimatch": "^3.1.2"
+ }
+ },
"@eslint/eslintrc": {
- "version": "2.1.4",
- "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz",
- "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==",
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz",
+ "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==",
"dev": true,
"requires": {
"ajv": "^6.12.4",
"debug": "^4.3.2",
- "espree": "^9.6.0",
- "globals": "^13.19.0",
+ "espree": "^10.0.1",
+ "globals": "^14.0.0",
"ignore": "^5.2.0",
"import-fresh": "^3.2.1",
"js-yaml": "^4.1.0",
@@ -7006,13 +7018,10 @@
"dev": true
},
"globals": {
- "version": "13.23.0",
- "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz",
- "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==",
- "dev": true,
- "requires": {
- "type-fest": "^0.20.2"
- }
+ "version": "14.0.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
+ "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
+ "dev": true
},
"js-yaml": {
"version": "4.1.0",
@@ -7026,20 +7035,24 @@
}
},
"@eslint/js": {
- "version": "8.57.0",
- "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz",
- "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==",
+ "version": "9.11.0",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.11.0.tgz",
+ "integrity": "sha512-LPkkenkDqyzTFauZLLAPhIb48fj6drrfMvRGSL9tS3AcZBSVTllemLSNyCvHNNL2t797S/6DJNSIwRwXgMO/eQ==",
+ "dev": true
+ },
+ "@eslint/object-schema": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz",
+ "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==",
"dev": true
},
- "@humanwhocodes/config-array": {
- "version": "0.11.14",
- "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
- "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==",
+ "@eslint/plugin-kit": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.0.tgz",
+ "integrity": "sha512-vH9PiIMMwvhCx31Af3HiGzsVNULDbyVkHXwlemn/B0TFj/00ho3y55efXrUZTfQipxoHC5u4xq6zblww1zm1Ig==",
"dev": true,
"requires": {
- "@humanwhocodes/object-schema": "^2.0.2",
- "debug": "^4.3.1",
- "minimatch": "^3.0.5"
+ "levn": "^0.4.1"
}
},
"@humanwhocodes/module-importer": {
@@ -7048,10 +7061,10 @@
"integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
"dev": true
},
- "@humanwhocodes/object-schema": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz",
- "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==",
+ "@humanwhocodes/retry": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.0.tgz",
+ "integrity": "sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==",
"dev": true
},
"@istanbuljs/load-nyc-config": {
@@ -7707,6 +7720,31 @@
"integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==",
"dev": true
},
+ "@types/eslint": {
+ "version": "9.6.1",
+ "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz",
+ "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==",
+ "dev": true,
+ "requires": {
+ "@types/estree": "*",
+ "@types/json-schema": "*"
+ }
+ },
+ "@types/eslint__js": {
+ "version": "8.42.3",
+ "resolved": "https://registry.npmjs.org/@types/eslint__js/-/eslint__js-8.42.3.tgz",
+ "integrity": "sha512-alfG737uhmPdnvkrLdZLcEKJ/B8s9Y4hrZ+YAdzUeoArBlSUERA2E87ROfOaS4jd/C45fzOoZzidLc1IPwLqOw==",
+ "dev": true,
+ "requires": {
+ "@types/eslint": "*"
+ }
+ },
+ "@types/estree": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
+ "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
+ "dev": true
+ },
"@types/graceful-fs": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz",
@@ -7781,20 +7819,14 @@
"dev": true
},
"@types/node": {
- "version": "22.0.2",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-22.0.2.tgz",
- "integrity": "sha512-yPL6DyFwY5PiMVEwymNeqUTKsDczQBJ/5T7W/46RwLU/VH+AA8aT5TZkvBviLKLbbm0hlfftEkGrNzfRk/fofQ==",
+ "version": "22.7.4",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.4.tgz",
+ "integrity": "sha512-y+NPi1rFzDs1NdQHHToqeiX2TIS79SWEAw9GYhkkx8bD0ChpfqC+n2j5OXOCpzfojBEBt6DnEnnG9MY0zk1XLg==",
"dev": true,
"requires": {
- "undici-types": "~6.11.1"
+ "undici-types": "~6.19.2"
}
},
- "@types/semver": {
- "version": "7.5.8",
- "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz",
- "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==",
- "dev": true
- },
"@types/stack-utils": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz",
@@ -7837,116 +7869,126 @@
"dev": true
},
"@typescript-eslint/eslint-plugin": {
- "version": "5.62.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz",
- "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==",
+ "version": "8.8.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.8.0.tgz",
+ "integrity": "sha512-wORFWjU30B2WJ/aXBfOm1LX9v9nyt9D3jsSOxC3cCaTQGCW5k4jNpmjFv3U7p/7s4yvdjHzwtv2Sd2dOyhjS0A==",
"dev": true,
"requires": {
- "@eslint-community/regexpp": "^4.4.0",
- "@typescript-eslint/scope-manager": "5.62.0",
- "@typescript-eslint/type-utils": "5.62.0",
- "@typescript-eslint/utils": "5.62.0",
- "debug": "^4.3.4",
+ "@eslint-community/regexpp": "^4.10.0",
+ "@typescript-eslint/scope-manager": "8.8.0",
+ "@typescript-eslint/type-utils": "8.8.0",
+ "@typescript-eslint/utils": "8.8.0",
+ "@typescript-eslint/visitor-keys": "8.8.0",
"graphemer": "^1.4.0",
- "ignore": "^5.2.0",
- "natural-compare-lite": "^1.4.0",
- "semver": "^7.3.7",
- "tsutils": "^3.21.0"
+ "ignore": "^5.3.1",
+ "natural-compare": "^1.4.0",
+ "ts-api-utils": "^1.3.0"
}
},
"@typescript-eslint/parser": {
- "version": "5.62.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz",
- "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==",
+ "version": "8.8.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.8.0.tgz",
+ "integrity": "sha512-uEFUsgR+tl8GmzmLjRqz+VrDv4eoaMqMXW7ruXfgThaAShO9JTciKpEsB+TvnfFfbg5IpujgMXVV36gOJRLtZg==",
"dev": true,
"requires": {
- "@typescript-eslint/scope-manager": "5.62.0",
- "@typescript-eslint/types": "5.62.0",
- "@typescript-eslint/typescript-estree": "5.62.0",
+ "@typescript-eslint/scope-manager": "8.8.0",
+ "@typescript-eslint/types": "8.8.0",
+ "@typescript-eslint/typescript-estree": "8.8.0",
+ "@typescript-eslint/visitor-keys": "8.8.0",
"debug": "^4.3.4"
}
},
"@typescript-eslint/scope-manager": {
- "version": "5.62.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz",
- "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==",
+ "version": "8.8.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.8.0.tgz",
+ "integrity": "sha512-EL8eaGC6gx3jDd8GwEFEV091210U97J0jeEHrAYvIYosmEGet4wJ+g0SYmLu+oRiAwbSA5AVrt6DxLHfdd+bUg==",
"dev": true,
"requires": {
- "@typescript-eslint/types": "5.62.0",
- "@typescript-eslint/visitor-keys": "5.62.0"
+ "@typescript-eslint/types": "8.8.0",
+ "@typescript-eslint/visitor-keys": "8.8.0"
}
},
"@typescript-eslint/type-utils": {
- "version": "5.62.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz",
- "integrity": "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==",
+ "version": "8.8.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.8.0.tgz",
+ "integrity": "sha512-IKwJSS7bCqyCeG4NVGxnOP6lLT9Okc3Zj8hLO96bpMkJab+10HIfJbMouLrlpyOr3yrQ1cA413YPFiGd1mW9/Q==",
"dev": true,
"requires": {
- "@typescript-eslint/typescript-estree": "5.62.0",
- "@typescript-eslint/utils": "5.62.0",
+ "@typescript-eslint/typescript-estree": "8.8.0",
+ "@typescript-eslint/utils": "8.8.0",
"debug": "^4.3.4",
- "tsutils": "^3.21.0"
+ "ts-api-utils": "^1.3.0"
}
},
"@typescript-eslint/types": {
- "version": "5.62.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz",
- "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==",
+ "version": "8.8.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.8.0.tgz",
+ "integrity": "sha512-QJwc50hRCgBd/k12sTykOJbESe1RrzmX6COk8Y525C9l7oweZ+1lw9JiU56im7Amm8swlz00DRIlxMYLizr2Vw==",
"dev": true
},
"@typescript-eslint/typescript-estree": {
- "version": "5.62.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz",
- "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==",
+ "version": "8.8.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.8.0.tgz",
+ "integrity": "sha512-ZaMJwc/0ckLz5DaAZ+pNLmHv8AMVGtfWxZe/x2JVEkD5LnmhWiQMMcYT7IY7gkdJuzJ9P14fRy28lUrlDSWYdw==",
"dev": true,
"requires": {
- "@typescript-eslint/types": "5.62.0",
- "@typescript-eslint/visitor-keys": "5.62.0",
+ "@typescript-eslint/types": "8.8.0",
+ "@typescript-eslint/visitor-keys": "8.8.0",
"debug": "^4.3.4",
- "globby": "^11.1.0",
+ "fast-glob": "^3.3.2",
"is-glob": "^4.0.3",
- "semver": "^7.3.7",
- "tsutils": "^3.21.0"
+ "minimatch": "^9.0.4",
+ "semver": "^7.6.0",
+ "ts-api-utils": "^1.3.0"
+ },
+ "dependencies": {
+ "brace-expansion": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+ "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+ "dev": true,
+ "requires": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "minimatch": {
+ "version": "9.0.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
+ "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+ "dev": true,
+ "requires": {
+ "brace-expansion": "^2.0.1"
+ }
+ }
}
},
"@typescript-eslint/utils": {
- "version": "5.62.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz",
- "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==",
+ "version": "8.8.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.8.0.tgz",
+ "integrity": "sha512-QE2MgfOTem00qrlPgyByaCHay9yb1+9BjnMFnSFkUKQfu7adBXDTnCAivURnuPPAG/qiB+kzKkZKmKfaMT0zVg==",
"dev": true,
"requires": {
- "@eslint-community/eslint-utils": "^4.2.0",
- "@types/json-schema": "^7.0.9",
- "@types/semver": "^7.3.12",
- "@typescript-eslint/scope-manager": "5.62.0",
- "@typescript-eslint/types": "5.62.0",
- "@typescript-eslint/typescript-estree": "5.62.0",
- "eslint-scope": "^5.1.1",
- "semver": "^7.3.7"
+ "@eslint-community/eslint-utils": "^4.4.0",
+ "@typescript-eslint/scope-manager": "8.8.0",
+ "@typescript-eslint/types": "8.8.0",
+ "@typescript-eslint/typescript-estree": "8.8.0"
}
},
"@typescript-eslint/visitor-keys": {
- "version": "5.62.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz",
- "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==",
+ "version": "8.8.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.8.0.tgz",
+ "integrity": "sha512-8mq51Lx6Hpmd7HnA2fcHQo3YgfX1qbccxQOgZcb4tvasu//zXRaA1j5ZRFeCw/VRAdFi4mRM9DnZw0Nu0Q2d1g==",
"dev": true,
"requires": {
- "@typescript-eslint/types": "5.62.0",
- "eslint-visitor-keys": "^3.3.0"
+ "@typescript-eslint/types": "8.8.0",
+ "eslint-visitor-keys": "^3.4.3"
}
},
- "@ungap/structured-clone": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz",
- "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==",
- "dev": true
- },
"acorn": {
- "version": "7.4.1",
- "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
- "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==",
- "dev": true,
- "peer": true
+ "version": "8.12.1",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz",
+ "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==",
+ "dev": true
},
"acorn-jsx": {
"version": "5.3.2",
@@ -8024,12 +8066,6 @@
"sprintf-js": "~1.0.2"
}
},
- "array-union": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
- "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
- "dev": true
- },
"async": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.3.tgz",
@@ -8381,24 +8417,6 @@
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
"dev": true
},
- "dir-glob": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
- "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
- "dev": true,
- "requires": {
- "path-type": "^4.0.0"
- }
- },
- "doctrine": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
- "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
- "dev": true,
- "requires": {
- "esutils": "^2.0.2"
- }
- },
"ecdsa-sig-formatter": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
@@ -8461,43 +8479,39 @@
"dev": true
},
"eslint": {
- "version": "8.57.0",
- "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz",
- "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==",
+ "version": "9.11.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.11.0.tgz",
+ "integrity": "sha512-yVS6XODx+tMFMDFcG4+Hlh+qG7RM6cCJXtQhCKLSsr3XkLvWggHjCqjfh0XsPPnt1c56oaT6PMgW9XWQQjdHXA==",
"dev": true,
"requires": {
"@eslint-community/eslint-utils": "^4.2.0",
- "@eslint-community/regexpp": "^4.6.1",
- "@eslint/eslintrc": "^2.1.4",
- "@eslint/js": "8.57.0",
- "@humanwhocodes/config-array": "^0.11.14",
+ "@eslint-community/regexpp": "^4.11.0",
+ "@eslint/config-array": "^0.18.0",
+ "@eslint/eslintrc": "^3.1.0",
+ "@eslint/js": "9.11.0",
+ "@eslint/plugin-kit": "^0.2.0",
"@humanwhocodes/module-importer": "^1.0.1",
+ "@humanwhocodes/retry": "^0.3.0",
"@nodelib/fs.walk": "^1.2.8",
- "@ungap/structured-clone": "^1.2.0",
"ajv": "^6.12.4",
"chalk": "^4.0.0",
"cross-spawn": "^7.0.2",
"debug": "^4.3.2",
- "doctrine": "^3.0.0",
"escape-string-regexp": "^4.0.0",
- "eslint-scope": "^7.2.2",
- "eslint-visitor-keys": "^3.4.3",
- "espree": "^9.6.1",
- "esquery": "^1.4.2",
+ "eslint-scope": "^8.0.2",
+ "eslint-visitor-keys": "^4.0.0",
+ "espree": "^10.1.0",
+ "esquery": "^1.5.0",
"esutils": "^2.0.2",
"fast-deep-equal": "^3.1.3",
- "file-entry-cache": "^6.0.1",
+ "file-entry-cache": "^8.0.0",
"find-up": "^5.0.0",
"glob-parent": "^6.0.2",
- "globals": "^13.19.0",
- "graphemer": "^1.4.0",
"ignore": "^5.2.0",
"imurmurhash": "^0.1.4",
"is-glob": "^4.0.0",
"is-path-inside": "^3.0.3",
- "js-yaml": "^4.1.0",
"json-stable-stringify-without-jsonify": "^1.0.1",
- "levn": "^0.4.1",
"lodash.merge": "^4.6.2",
"minimatch": "^3.1.2",
"natural-compare": "^1.4.0",
@@ -8506,32 +8520,16 @@
"text-table": "^0.2.0"
},
"dependencies": {
- "argparse": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
- "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
- "dev": true
- },
"escape-string-regexp": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
"dev": true
},
- "eslint-scope": {
- "version": "7.2.2",
- "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
- "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==",
- "dev": true,
- "requires": {
- "esrecurse": "^4.3.0",
- "estraverse": "^5.2.0"
- }
- },
- "estraverse": {
- "version": "5.3.0",
- "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
- "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "eslint-visitor-keys": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz",
+ "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==",
"dev": true
},
"find-up": {
@@ -8553,34 +8551,6 @@
"is-glob": "^4.0.3"
}
},
- "globals": {
- "version": "13.19.0",
- "resolved": "https://registry.npmjs.org/globals/-/globals-13.19.0.tgz",
- "integrity": "sha512-dkQ957uSRWHw7CFXLUtUHQI3g3aWApYhfNR2O6jn/907riyTYKVBmxYVROkBcY614FSSeSJh7Xm7SrUWCxvJMQ==",
- "dev": true,
- "requires": {
- "type-fest": "^0.20.2"
- }
- },
- "js-yaml": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
- "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
- "dev": true,
- "requires": {
- "argparse": "^2.0.1"
- }
- },
- "levn": {
- "version": "0.4.1",
- "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
- "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
- "dev": true,
- "requires": {
- "prelude-ls": "^1.2.1",
- "type-check": "~0.4.0"
- }
- },
"locate-path": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
@@ -8621,21 +8591,6 @@
"requires": {
"p-limit": "^3.0.2"
}
- },
- "prelude-ls": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
- "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
- "dev": true
- },
- "type-check": {
- "version": "0.4.0",
- "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
- "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
- "dev": true,
- "requires": {
- "prelude-ls": "^1.2.1"
- }
}
}
},
@@ -8647,22 +8602,22 @@
"requires": {}
},
"eslint-plugin-lodash": {
- "version": "7.4.0",
- "resolved": "https://registry.npmjs.org/eslint-plugin-lodash/-/eslint-plugin-lodash-7.4.0.tgz",
- "integrity": "sha512-Tl83UwVXqe1OVeBRKUeWcfg6/pCW1GTRObbdnbEJgYwjxp5Q92MEWQaH9+dmzbRt6kvYU1Mp893E79nJiCSM8A==",
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-lodash/-/eslint-plugin-lodash-8.0.0.tgz",
+ "integrity": "sha512-7DA8485FolmWRzh+8t4S8Pzin2TTuWfb0ZW3j/2fYElgk82ZanFz8vDcvc4BBPceYdX1p/za+tkbO68maDBGGw==",
"dev": true,
"requires": {
"lodash": "^4.17.21"
}
},
"eslint-scope": {
- "version": "5.1.1",
- "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
- "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==",
+ "version": "8.0.2",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.0.2.tgz",
+ "integrity": "sha512-6E4xmrTw5wtxnLA5wYL3WDfhZ/1bUBGOXV0zQvVRDOtrR8D0p6W7fs3JweNYhwRYeGvd/1CKX2se0/2s7Q/nJA==",
"dev": true,
"requires": {
"esrecurse": "^4.3.0",
- "estraverse": "^4.1.1"
+ "estraverse": "^5.2.0"
}
},
"eslint-visitor-keys": {
@@ -8672,20 +8627,20 @@
"dev": true
},
"espree": {
- "version": "9.6.1",
- "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz",
- "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==",
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-10.1.0.tgz",
+ "integrity": "sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==",
"dev": true,
"requires": {
- "acorn": "^8.9.0",
+ "acorn": "^8.12.0",
"acorn-jsx": "^5.3.2",
- "eslint-visitor-keys": "^3.4.1"
+ "eslint-visitor-keys": "^4.0.0"
},
"dependencies": {
- "acorn": {
- "version": "8.11.2",
- "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz",
- "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==",
+ "eslint-visitor-keys": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz",
+ "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==",
"dev": true
}
}
@@ -8697,20 +8652,12 @@
"dev": true
},
"esquery": {
- "version": "1.4.2",
- "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.2.tgz",
- "integrity": "sha512-JVSoLdTlTDkmjFmab7H/9SL9qGSyjElT3myyKp7krqjVFQCDLmj1QFaCLRFBszBKI0XVZaiiXvuPIX3ZwHe1Ng==",
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz",
+ "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==",
"dev": true,
"requires": {
"estraverse": "^5.1.0"
- },
- "dependencies": {
- "estraverse": {
- "version": "5.3.0",
- "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
- "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
- "dev": true
- }
}
},
"esrecurse": {
@@ -8720,20 +8667,12 @@
"dev": true,
"requires": {
"estraverse": "^5.2.0"
- },
- "dependencies": {
- "estraverse": {
- "version": "5.2.0",
- "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz",
- "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==",
- "dev": true
- }
}
},
"estraverse": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
- "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
"dev": true
},
"esutils": {
@@ -8793,9 +8732,9 @@
"dev": true
},
"fast-glob": {
- "version": "3.2.12",
- "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz",
- "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==",
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
+ "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==",
"dev": true,
"requires": {
"@nodelib/fs.stat": "^2.0.2",
@@ -8841,12 +8780,12 @@
"integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw=="
},
"file-entry-cache": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
- "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==",
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
+ "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
"dev": true,
"requires": {
- "flat-cache": "^3.0.4"
+ "flat-cache": "^4.0.0"
}
},
"file-uri-to-path": {
@@ -8904,19 +8843,19 @@
}
},
"flat-cache": {
- "version": "3.0.4",
- "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz",
- "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==",
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
+ "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
"dev": true,
"requires": {
- "flatted": "^3.1.0",
- "rimraf": "^3.0.2"
+ "flatted": "^3.2.9",
+ "keyv": "^4.5.4"
}
},
"flatted": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.1.1.tgz",
- "integrity": "sha512-zAoAQiudy+r5SvnSw3KJy5os/oRJYHzrzja/tBDqrZtNhUw8bt6y8OBzMWcjWr+8liV8Eb6yOhw8WZ7VFZ5ZzA==",
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz",
+ "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==",
"dev": true
},
"fn.name": {
@@ -8991,25 +8930,11 @@
}
},
"globals": {
- "version": "11.12.0",
- "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
- "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
+ "version": "15.9.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-15.9.0.tgz",
+ "integrity": "sha512-SmSKyLLKFbSr6rptvP8izbyxJL4ILwqO9Jg23UA0sDlGlu58V59D1//I3vlc0KJphVdUR7vMjHIplYnzBxorQA==",
"dev": true
},
- "globby": {
- "version": "11.1.0",
- "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz",
- "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==",
- "dev": true,
- "requires": {
- "array-union": "^2.1.0",
- "dir-glob": "^3.0.1",
- "fast-glob": "^3.2.9",
- "ignore": "^5.2.0",
- "merge2": "^1.4.1",
- "slash": "^3.0.0"
- }
- },
"graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
@@ -9058,9 +8983,9 @@
"dev": true
},
"ignore": {
- "version": "5.2.1",
- "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.1.tgz",
- "integrity": "sha512-d2qQLzTJ9WxQftPAuEQpSPmKqzxePjzVbpAVv62AQ64NTL+wR4JkrVqR/LqFsFEUsHDAiId52mJteHDFuDkElA==",
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
"dev": true
},
"import-fresh": {
@@ -10234,6 +10159,12 @@
"integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==",
"dev": true
},
+ "json-buffer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+ "dev": true
+ },
"json-parse-even-better-errors": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
@@ -10294,6 +10225,15 @@
"safe-buffer": "^5.0.1"
}
},
+ "keyv": {
+ "version": "4.5.4",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+ "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+ "dev": true,
+ "requires": {
+ "json-buffer": "3.0.1"
+ }
+ },
"kleur": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz",
@@ -10311,6 +10251,16 @@
"integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==",
"dev": true
},
+ "levn": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
+ "requires": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ }
+ },
"lines-and-columns": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
@@ -10443,13 +10393,13 @@
"dev": true
},
"micromatch": {
- "version": "4.0.4",
- "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz",
- "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==",
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+ "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"dev": true,
"requires": {
- "braces": "^3.0.1",
- "picomatch": "^2.2.3"
+ "braces": "^3.0.3",
+ "picomatch": "^2.3.1"
}
},
"mimic-fn": {
@@ -10484,12 +10434,6 @@
"integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=",
"dev": true
},
- "natural-compare-lite": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz",
- "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==",
- "dev": true
- },
"node-int64": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
@@ -10617,12 +10561,6 @@
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"dev": true
},
- "path-type": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
- "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
- "dev": true
- },
"picocolors": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
@@ -10630,9 +10568,9 @@
"dev": true
},
"picomatch": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz",
- "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==",
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dev": true
},
"pirates": {
@@ -10650,6 +10588,12 @@
"find-up": "^4.0.0"
}
},
+ "prelude-ls": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+ "dev": true
+ },
"prettier": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz",
@@ -10686,9 +10630,9 @@
}
},
"punycode": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
- "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
"dev": true
},
"pure-rand": {
@@ -10781,15 +10725,6 @@
"integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
"dev": true
},
- "rimraf": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
- "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
- "dev": true,
- "requires": {
- "glob": "^7.1.3"
- }
- },
"run-parallel": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
@@ -11029,6 +10964,13 @@
"resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz",
"integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg=="
},
+ "ts-api-utils": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz",
+ "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==",
+ "dev": true,
+ "requires": {}
+ },
"ts-jest": {
"version": "29.2.5",
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.5.tgz",
@@ -11067,12 +11009,6 @@
"yn": "3.1.1"
},
"dependencies": {
- "acorn": {
- "version": "8.8.1",
- "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.1.tgz",
- "integrity": "sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==",
- "dev": true
- },
"acorn-walk": {
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz",
@@ -11081,19 +11017,13 @@
}
}
},
- "tslib": {
- "version": "1.14.1",
- "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
- "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
- "dev": true
- },
- "tsutils": {
- "version": "3.21.0",
- "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz",
- "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==",
+ "type-check": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
"dev": true,
"requires": {
- "tslib": "^1.8.1"
+ "prelude-ls": "^1.2.1"
}
},
"type-detect": {
@@ -11102,22 +11032,27 @@
"integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==",
"dev": true
},
- "type-fest": {
- "version": "0.20.2",
- "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
- "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
- "dev": true
- },
"typescript": {
"version": "5.5.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz",
"integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==",
"dev": true
},
+ "typescript-eslint": {
+ "version": "8.8.0",
+ "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.8.0.tgz",
+ "integrity": "sha512-BjIT/VwJ8+0rVO01ZQ2ZVnjE1svFBiRczcpr1t1Yxt7sT25VSbPfrJtDsQ8uQTy2pilX5nI9gwxhUyLULNentw==",
+ "dev": true,
+ "requires": {
+ "@typescript-eslint/eslint-plugin": "8.8.0",
+ "@typescript-eslint/parser": "8.8.0",
+ "@typescript-eslint/utils": "8.8.0"
+ }
+ },
"undici-types": {
- "version": "6.11.1",
- "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.11.1.tgz",
- "integrity": "sha512-mIDEX2ek50x0OlRgxryxsenE5XaQD4on5U2inY7RApK3SOJpofyw7uW2AyfMKkhAxXIceo2DeWGVGwyvng1GNQ==",
+ "version": "6.19.8",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
+ "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
"dev": true
},
"unix-dgram": {
diff --git a/superset-websocket/package.json b/superset-websocket/package.json
index bea3f0713889e..d8ab1c46c6827 100644
--- a/superset-websocket/package.json
+++ b/superset-websocket/package.json
@@ -7,7 +7,7 @@
"start": "node dist/index.js start",
"test": "NODE_ENV=test jest -i spec",
"type": "tsc --noEmit",
- "eslint": "eslint --ext .js,.jsx,.ts,.tsx",
+ "eslint": "eslint",
"lint": "npm run eslint -- . && npm run type",
"dev-server": "ts-node src/index.ts start",
"build": "tsc",
@@ -17,7 +17,6 @@
},
"license": "Apache-2.0",
"dependencies": {
- "@types/lodash": "^4.17.7",
"cookie": "^0.6.0",
"hot-shots": "^10.0.0",
"ioredis": "^4.28.0",
@@ -28,24 +27,28 @@
"ws": "^8.18.0"
},
"devDependencies": {
+ "@eslint/js": "^9.11.0",
"@types/cookie": "^0.6.0",
+ "@types/eslint__js": "^8.42.3",
"@types/ioredis": "^4.27.8",
- "@types/lodash": "^4.17.7",
"@types/jest": "^29.5.12",
"@types/jsonwebtoken": "^9.0.6",
- "@types/node": "^22.0.2",
+ "@types/lodash": "^4.17.7",
+ "@types/node": "^22.7.4",
"@types/uuid": "^10.0.0",
"@types/ws": "^8.5.12",
- "@typescript-eslint/eslint-plugin": "^5.62.0",
- "@typescript-eslint/parser": "^5.62.0",
- "eslint": "^8.57.0",
+ "@typescript-eslint/eslint-plugin": "^8.8.0",
+ "@typescript-eslint/parser": "^8.6.0",
+ "eslint": "^9.11.0",
"eslint-config-prettier": "^9.1.0",
- "eslint-plugin-lodash": "^7.4.0",
+ "eslint-plugin-lodash": "^8.0.0",
+ "globals": "^15.9.0",
"jest": "^29.7.0",
"prettier": "^3.3.3",
"ts-jest": "^29.2.5",
"ts-node": "^10.9.2",
- "typescript": "^5.5.4"
+ "typescript": "^5.5.4",
+ "typescript-eslint": "^8.8.0"
},
"engines": {
"node": "^16.9.1",
diff --git a/superset-websocket/spec/index.test.ts b/superset-websocket/spec/index.test.ts
index 1eb6e7cc5246e..1643c9f6ac8b1 100644
--- a/superset-websocket/spec/index.test.ts
+++ b/superset-websocket/spec/index.test.ts
@@ -89,7 +89,10 @@ describe('server', () => {
end: endMock,
};
- server.httpRequest(request as any, response as any);
+ server.httpRequest(
+ request as unknown as http.IncomingMessage,
+ response as unknown as http.ServerResponse,
+ );
expect(writeHeadMock).toBeCalledTimes(1);
expect(writeHeadMock).toHaveBeenLastCalledWith(200);
@@ -115,7 +118,10 @@ describe('server', () => {
end: endMock,
};
- server.httpRequest(request as any, response as any);
+ server.httpRequest(
+ request as unknown as http.IncomingMessage,
+ response as unknown as http.ServerResponse,
+ );
expect(writeHeadMock).toBeCalledTimes(1);
expect(writeHeadMock).toHaveBeenLastCalledWith(404);
diff --git a/superset-websocket/src/config.ts b/superset-websocket/src/config.ts
index 0c6dc7440d304..c739ad2beaa72 100644
--- a/superset-websocket/src/config.ts
+++ b/superset-websocket/src/config.ts
@@ -90,7 +90,7 @@ function configFromFile(): Partial {
const configFile = isTest ? '../config.test.json' : '../config.json';
try {
return require(configFile);
- } catch (err) {
+ } catch {
console.warn('config.json file not found');
return {};
}
diff --git a/superset-websocket/src/index.ts b/superset-websocket/src/index.ts
index 96a76e0a19471..5c3f49fc17abc 100644
--- a/superset-websocket/src/index.ts
+++ b/superset-websocket/src/index.ts
@@ -36,6 +36,7 @@ export type StreamResult = [
// sync with superset-frontend/src/components/ErrorMessage/types
export type ErrorLevel = 'info' | 'warning' | 'error';
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type SupersetError | null> = {
error_type: string;
extra: ExtraType;
diff --git a/superset-websocket/utils/client-ws-app/package-lock.json b/superset-websocket/utils/client-ws-app/package-lock.json
index d6ff5a8402164..278d43436b688 100644
--- a/superset-websocket/utils/client-ws-app/package-lock.json
+++ b/superset-websocket/utils/client-ws-app/package-lock.json
@@ -9,8 +9,8 @@
"version": "0.0.0",
"dependencies": {
"cookie-parser": "~1.4.6",
- "debug": "~4.3.6",
- "express": "~4.19.2",
+ "debug": "~4.3.7",
+ "express": "~4.20.0",
"http-errors": "~2.0.0",
"jsonwebtoken": "^9.0.2",
"morgan": "~1.10.0",
@@ -109,9 +109,9 @@
}
},
"node_modules/body-parser": {
- "version": "1.20.2",
- "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz",
- "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==",
+ "version": "1.20.3",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
+ "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
"dependencies": {
"bytes": "3.1.2",
"content-type": "~1.0.5",
@@ -121,7 +121,7 @@
"http-errors": "2.0.0",
"iconv-lite": "0.4.24",
"on-finished": "2.4.1",
- "qs": "6.11.0",
+ "qs": "6.13.0",
"raw-body": "2.5.2",
"type-is": "~1.6.18",
"unpipe": "1.0.0"
@@ -155,6 +155,20 @@
"node": ">= 0.8"
}
},
+ "node_modules/body-parser/node_modules/qs": {
+ "version": "6.13.0",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
+ "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
+ "dependencies": {
+ "side-channel": "^1.0.6"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/buffer-equal-constant-time": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
@@ -275,11 +289,11 @@
"integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw="
},
"node_modules/debug": {
- "version": "4.3.6",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz",
- "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==",
+ "version": "4.3.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
+ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"dependencies": {
- "ms": "2.1.2"
+ "ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
@@ -382,36 +396,36 @@
}
},
"node_modules/express": {
- "version": "4.19.2",
- "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz",
- "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==",
+ "version": "4.20.0",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.20.0.tgz",
+ "integrity": "sha512-pLdae7I6QqShF5PnNTCVn4hI91Dx0Grkn2+IAsMTgMIKuQVte2dN9PeGSSAME2FR8anOhVA62QDIUaWVfEXVLw==",
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
- "body-parser": "1.20.2",
+ "body-parser": "1.20.3",
"content-disposition": "0.5.4",
"content-type": "~1.0.4",
"cookie": "0.6.0",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "2.0.0",
- "encodeurl": "~1.0.2",
+ "encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"finalhandler": "1.2.0",
"fresh": "0.5.2",
"http-errors": "2.0.0",
- "merge-descriptors": "1.0.1",
+ "merge-descriptors": "1.0.3",
"methods": "~1.1.2",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
- "path-to-regexp": "0.1.7",
+ "path-to-regexp": "0.1.10",
"proxy-addr": "~2.0.7",
"qs": "6.11.0",
"range-parser": "~1.2.1",
"safe-buffer": "5.2.1",
- "send": "0.18.0",
- "serve-static": "1.15.0",
+ "send": "0.19.0",
+ "serve-static": "1.16.0",
"setprototypeof": "1.2.0",
"statuses": "2.0.1",
"type-is": "~1.6.18",
@@ -430,6 +444,14 @@
"ms": "2.0.0"
}
},
+ "node_modules/express/node_modules/encodeurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/express/node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
@@ -733,11 +755,6 @@
"npm": ">=6"
}
},
- "node_modules/jsonwebtoken/node_modules/ms": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
- },
"node_modules/jstransformer": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/jstransformer/-/jstransformer-1.0.0.tgz",
@@ -821,9 +838,12 @@
}
},
"node_modules/merge-descriptors": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
- "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E="
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
+ "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
},
"node_modules/methods": {
"version": "1.1.2",
@@ -892,9 +912,9 @@
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
},
"node_modules/ms": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
- "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
"node_modules/negotiator": {
"version": "0.6.3",
@@ -913,9 +933,12 @@
}
},
"node_modules/object-inspect": {
- "version": "1.13.1",
- "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz",
- "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==",
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz",
+ "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==",
+ "engines": {
+ "node": ">= 0.4"
+ },
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
@@ -953,9 +976,9 @@
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
},
"node_modules/path-to-regexp": {
- "version": "0.1.7",
- "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
- "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
+ "version": "0.1.10",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz",
+ "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w=="
},
"node_modules/promise": {
"version": "7.3.1",
@@ -1162,9 +1185,9 @@
}
},
"node_modules/send": {
- "version": "0.18.0",
- "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
- "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
+ "version": "0.19.0",
+ "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
+ "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
"dependencies": {
"debug": "2.6.9",
"depd": "2.0.0",
@@ -1197,11 +1220,6 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
},
- "node_modules/send/node_modules/ms": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
- },
"node_modules/send/node_modules/on-finished": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
@@ -1214,9 +1232,9 @@
}
},
"node_modules/serve-static": {
- "version": "1.15.0",
- "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
- "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==",
+ "version": "1.16.0",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.0.tgz",
+ "integrity": "sha512-pDLK8zwl2eKaYrs8mrPZBJua4hMplRWJ1tIFksVC3FtBEBnl8dxgeHtsaMS8DhS9i4fLObaon6ABoc4/hQGdPA==",
"dependencies": {
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
@@ -1227,6 +1245,53 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/serve-static/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/serve-static/node_modules/debug/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
+ },
+ "node_modules/serve-static/node_modules/on-finished": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/serve-static/node_modules/send": {
+ "version": "0.18.0",
+ "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
+ "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
+ "dependencies": {
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "fresh": "0.5.2",
+ "http-errors": "2.0.0",
+ "mime": "1.6.0",
+ "ms": "2.1.3",
+ "on-finished": "2.4.1",
+ "range-parser": "~1.2.1",
+ "statuses": "2.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
"node_modules/set-function-length": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.1.tgz",
@@ -1424,9 +1489,9 @@
}
},
"body-parser": {
- "version": "1.20.2",
- "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz",
- "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==",
+ "version": "1.20.3",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
+ "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
"requires": {
"bytes": "3.1.2",
"content-type": "~1.0.5",
@@ -1436,7 +1501,7 @@
"http-errors": "2.0.0",
"iconv-lite": "0.4.24",
"on-finished": "2.4.1",
- "qs": "6.11.0",
+ "qs": "6.13.0",
"raw-body": "2.5.2",
"type-is": "~1.6.18",
"unpipe": "1.0.0"
@@ -1462,6 +1527,14 @@
"requires": {
"ee-first": "1.1.1"
}
+ },
+ "qs": {
+ "version": "6.13.0",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
+ "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
+ "requires": {
+ "side-channel": "^1.0.6"
+ }
}
}
},
@@ -1551,11 +1624,11 @@
"integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw="
},
"debug": {
- "version": "4.3.6",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz",
- "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==",
+ "version": "4.3.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
+ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"requires": {
- "ms": "2.1.2"
+ "ms": "^2.1.3"
}
},
"define-data-property": {
@@ -1625,36 +1698,36 @@
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="
},
"express": {
- "version": "4.19.2",
- "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz",
- "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==",
+ "version": "4.20.0",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.20.0.tgz",
+ "integrity": "sha512-pLdae7I6QqShF5PnNTCVn4hI91Dx0Grkn2+IAsMTgMIKuQVte2dN9PeGSSAME2FR8anOhVA62QDIUaWVfEXVLw==",
"requires": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
- "body-parser": "1.20.2",
+ "body-parser": "1.20.3",
"content-disposition": "0.5.4",
"content-type": "~1.0.4",
"cookie": "0.6.0",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "2.0.0",
- "encodeurl": "~1.0.2",
+ "encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"finalhandler": "1.2.0",
"fresh": "0.5.2",
"http-errors": "2.0.0",
- "merge-descriptors": "1.0.1",
+ "merge-descriptors": "1.0.3",
"methods": "~1.1.2",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
- "path-to-regexp": "0.1.7",
+ "path-to-regexp": "0.1.10",
"proxy-addr": "~2.0.7",
"qs": "6.11.0",
"range-parser": "~1.2.1",
"safe-buffer": "5.2.1",
- "send": "0.18.0",
- "serve-static": "1.15.0",
+ "send": "0.19.0",
+ "serve-static": "1.16.0",
"setprototypeof": "1.2.0",
"statuses": "2.0.1",
"type-is": "~1.6.18",
@@ -1670,6 +1743,11 @@
"ms": "2.0.0"
}
},
+ "encodeurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="
+ },
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
@@ -1885,13 +1963,6 @@
"lodash.once": "^4.0.0",
"ms": "^2.1.1",
"semver": "^7.5.4"
- },
- "dependencies": {
- "ms": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
- }
}
},
"jstransformer": {
@@ -1971,9 +2042,9 @@
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="
},
"merge-descriptors": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
- "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E="
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
+ "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ=="
},
"methods": {
"version": "1.1.2",
@@ -2026,9 +2097,9 @@
}
},
"ms": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
- "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
"negotiator": {
"version": "0.6.3",
@@ -2041,9 +2112,9 @@
"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
},
"object-inspect": {
- "version": "1.13.1",
- "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz",
- "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ=="
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz",
+ "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g=="
},
"on-finished": {
"version": "2.3.0",
@@ -2069,9 +2140,9 @@
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
},
"path-to-regexp": {
- "version": "0.1.7",
- "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
- "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
+ "version": "0.1.10",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz",
+ "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w=="
},
"promise": {
"version": "7.3.1",
@@ -2254,9 +2325,9 @@
}
},
"send": {
- "version": "0.18.0",
- "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
- "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
+ "version": "0.19.0",
+ "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
+ "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
"requires": {
"debug": "2.6.9",
"depd": "2.0.0",
@@ -2288,11 +2359,6 @@
}
}
},
- "ms": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
- },
"on-finished": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
@@ -2304,14 +2370,59 @@
}
},
"serve-static": {
- "version": "1.15.0",
- "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
- "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==",
+ "version": "1.16.0",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.0.tgz",
+ "integrity": "sha512-pDLK8zwl2eKaYrs8mrPZBJua4hMplRWJ1tIFksVC3FtBEBnl8dxgeHtsaMS8DhS9i4fLObaon6ABoc4/hQGdPA==",
"requires": {
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"parseurl": "~1.3.3",
"send": "0.18.0"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "requires": {
+ "ms": "2.0.0"
+ },
+ "dependencies": {
+ "ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
+ }
+ }
+ },
+ "on-finished": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+ "requires": {
+ "ee-first": "1.1.1"
+ }
+ },
+ "send": {
+ "version": "0.18.0",
+ "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
+ "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
+ "requires": {
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "fresh": "0.5.2",
+ "http-errors": "2.0.0",
+ "mime": "1.6.0",
+ "ms": "2.1.3",
+ "on-finished": "2.4.1",
+ "range-parser": "~1.2.1",
+ "statuses": "2.0.1"
+ }
+ }
}
},
"set-function-length": {
diff --git a/superset-websocket/utils/client-ws-app/package.json b/superset-websocket/utils/client-ws-app/package.json
index fda145a9d7ea6..c0c2aafb2274a 100644
--- a/superset-websocket/utils/client-ws-app/package.json
+++ b/superset-websocket/utils/client-ws-app/package.json
@@ -7,8 +7,8 @@
},
"dependencies": {
"cookie-parser": "~1.4.6",
- "debug": "~4.3.6",
- "express": "~4.19.2",
+ "debug": "~4.3.7",
+ "express": "~4.20.0",
"http-errors": "~2.0.0",
"jsonwebtoken": "^9.0.2",
"morgan": "~1.10.0",
diff --git a/superset/charts/api.py b/superset/charts/api.py
index 57a812fb74aba..64177950b8437 100644
--- a/superset/charts/api.py
+++ b/superset/charts/api.py
@@ -66,7 +66,9 @@
DashboardsForbiddenError,
)
from superset.commands.chart.export import ExportChartsCommand
+from superset.commands.chart.fave import AddFavoriteChartCommand
from superset.commands.chart.importers.dispatcher import ImportChartsCommand
+from superset.commands.chart.unfave import DelFavoriteChartCommand
from superset.commands.chart.update import UpdateChartCommand
from superset.commands.chart.warm_up_cache import ChartWarmUpCacheCommand
from superset.commands.exceptions import CommandException, TagForbiddenError
@@ -898,11 +900,13 @@ def add_favorite(self, pk: int) -> Response:
500:
$ref: '#/components/responses/500'
"""
- chart = ChartDAO.find_by_id(pk)
- if not chart:
+ try:
+ AddFavoriteChartCommand(pk).run()
+ except ChartNotFoundError:
return self.response_404()
+ except ChartForbiddenError:
+ return self.response_403()
- ChartDAO.add_favorite(chart)
return self.response(200, result="OK")
@expose("//favorites/", methods=("DELETE",))
@@ -941,11 +945,13 @@ def remove_favorite(self, pk: int) -> Response:
500:
$ref: '#/components/responses/500'
"""
- chart = ChartDAO.find_by_id(pk)
- if not chart:
- return self.response_404()
+ try:
+ DelFavoriteChartCommand(pk).run()
+ except ChartNotFoundError:
+ self.response_404()
+ except ChartForbiddenError:
+ self.response_403()
- ChartDAO.remove_favorite(chart)
return self.response(200, result="OK")
@expose("/warm_up_cache", methods=("PUT",))
diff --git a/superset/charts/post_processing.py b/superset/charts/post_processing.py
index ebcae32f8f486..4c5abd8db19f1 100644
--- a/superset/charts/post_processing.py
+++ b/superset/charts/post_processing.py
@@ -29,6 +29,7 @@
from io import StringIO
from typing import Any, Optional, TYPE_CHECKING, Union
+import numpy as np
import pandas as pd
from flask_babel import gettext as __
@@ -83,10 +84,11 @@ def pivot_df( # pylint: disable=too-many-locals, too-many-arguments, too-many-s
else:
axis = {"columns": 1, "rows": 0}
+ # pivoting with null values will create an empty df
+ df = df.fillna("SUPERSET_PANDAS_NAN")
+
# pivot data; we'll compute totals and subtotals later
if rows or columns:
- # pivoting with null values will create an empty df
- df = df.fillna("NULL")
df = df.pivot_table(
index=rows,
columns=columns,
@@ -151,6 +153,18 @@ def pivot_df( # pylint: disable=too-many-locals, too-many-arguments, too-many-s
# add subtotal for each group and overall total; we start from the
# overall group, and iterate deeper into subgroups
groups = df.columns
+ if not apply_metrics_on_rows:
+ for col in df.columns:
+ # we need to replace the temporary placeholder with either a string
+ # or np.nan, depending on the column type so that they can sum correctly
+ if pd.api.types.is_numeric_dtype(df[col]):
+ df[col].replace("SUPERSET_PANDAS_NAN", np.nan, inplace=True)
+ else:
+ df[col].replace("SUPERSET_PANDAS_NAN", "nan", inplace=True)
+ else:
+ # when we applied metrics on rows, we switched the columns and rows
+ # so checking column type doesn't apply. Replace everything with np.nan
+ df.replace("SUPERSET_PANDAS_NAN", np.nan, inplace=True)
for level in range(df.columns.nlevels):
subgroups = {group[:level] for group in groups}
for subgroup in subgroups:
@@ -171,7 +185,7 @@ def pivot_df( # pylint: disable=too-many-locals, too-many-arguments, too-many-s
for subgroup in subgroups:
slice_ = df.index.get_loc(subgroup)
subtotal = pivot_v2_aggfunc_map[aggfunc](
- df.iloc[slice_, :].apply(pd.to_numeric), axis=0
+ df.iloc[slice_, :].apply(pd.to_numeric, errors="coerce"), axis=0
)
depth = df.index.nlevels - len(subgroup) - 1
total = metric_name if level == 0 else __("Subtotal")
@@ -186,6 +200,14 @@ def pivot_df( # pylint: disable=too-many-locals, too-many-arguments, too-many-s
if apply_metrics_on_rows:
df = df.T
+ # replace the remaining temporary placeholder string for np.nan after pivoting
+ df.replace("SUPERSET_PANDAS_NAN", np.nan, inplace=True)
+ df.rename(
+ index={"SUPERSET_PANDAS_NAN": np.nan},
+ columns={"SUPERSET_PANDAS_NAN": np.nan},
+ inplace=True,
+ )
+
return df
diff --git a/superset/commands/chart/exceptions.py b/superset/commands/chart/exceptions.py
index 00877aa803070..72ef71f466e8e 100644
--- a/superset/commands/chart/exceptions.py
+++ b/superset/commands/chart/exceptions.py
@@ -154,3 +154,11 @@ class DashboardsForbiddenError(ForbiddenError):
class WarmUpCacheChartNotFoundError(CommandException):
status = 404
message = _("Chart not found")
+
+
+class ChartFaveError(CommandException):
+ message = _("Error faving chart")
+
+
+class ChartUnfaveError(CommandException):
+ message = _("Error unfaving chart")
diff --git a/superset/commands/chart/fave.py b/superset/commands/chart/fave.py
new file mode 100644
index 0000000000000..d45d1694761c3
--- /dev/null
+++ b/superset/commands/chart/fave.py
@@ -0,0 +1,57 @@
+# 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.
+import logging
+from functools import partial
+
+from requests_cache import Optional
+
+from superset import security_manager
+from superset.commands.base import BaseCommand
+from superset.commands.chart.exceptions import (
+ ChartFaveError,
+ ChartForbiddenError,
+ ChartNotFoundError,
+)
+from superset.daos.chart import ChartDAO
+from superset.exceptions import SupersetSecurityException
+from superset.models.slice import Slice
+from superset.utils.decorators import on_error, transaction
+
+logger = logging.getLogger(__name__)
+
+
+class AddFavoriteChartCommand(BaseCommand):
+ def __init__(self, chart_id: int) -> None:
+ self._chart_id = chart_id
+ self._chart: Optional[Slice] = None
+
+ @transaction(on_error=partial(on_error, reraise=ChartFaveError))
+ def run(self) -> None:
+ self.validate()
+ return ChartDAO.add_favorite(self._chart)
+
+ def validate(self) -> None:
+ chart = ChartDAO.find_by_id(self._chart_id)
+ if not chart:
+ raise ChartNotFoundError()
+
+ try:
+ security_manager.raise_for_ownership(chart)
+ except SupersetSecurityException as ex:
+ raise ChartForbiddenError() from ex
+
+ self._chart = chart
diff --git a/superset/commands/chart/unfave.py b/superset/commands/chart/unfave.py
new file mode 100644
index 0000000000000..d19d2b2769993
--- /dev/null
+++ b/superset/commands/chart/unfave.py
@@ -0,0 +1,57 @@
+# 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.
+import logging
+from functools import partial
+
+from requests_cache import Optional
+
+from superset import security_manager
+from superset.commands.base import BaseCommand
+from superset.commands.chart.exceptions import (
+ ChartForbiddenError,
+ ChartNotFoundError,
+ ChartUnfaveError,
+)
+from superset.daos.chart import ChartDAO
+from superset.exceptions import SupersetSecurityException
+from superset.models.slice import Slice
+from superset.utils.decorators import on_error, transaction
+
+logger = logging.getLogger(__name__)
+
+
+class DelFavoriteChartCommand(BaseCommand):
+ def __init__(self, chart_id: int) -> None:
+ self._chart_id = chart_id
+ self._chart: Optional[Slice] = None
+
+ @transaction(on_error=partial(on_error, reraise=ChartUnfaveError))
+ def run(self) -> None:
+ self.validate()
+ return ChartDAO.remove_favorite(self._chart)
+
+ def validate(self) -> None:
+ chart = ChartDAO.find_by_id(self._chart_id)
+ if not chart:
+ raise ChartNotFoundError()
+
+ try:
+ security_manager.raise_for_ownership(chart)
+ except SupersetSecurityException as ex:
+ raise ChartForbiddenError() from ex
+
+ self._chart = chart
diff --git a/superset/commands/dashboard/exceptions.py b/superset/commands/dashboard/exceptions.py
index 91a4ec6832222..9281119b320bf 100644
--- a/superset/commands/dashboard/exceptions.py
+++ b/superset/commands/dashboard/exceptions.py
@@ -84,3 +84,11 @@ class DashboardAccessDeniedError(ForbiddenError):
class DashboardCopyError(CommandInvalidError):
message = _("Dashboard cannot be copied due to invalid parameters.")
+
+
+class DashboardFaveError(CommandInvalidError):
+ message = _("Dashboard cannot be favorited.")
+
+
+class DashboardUnfaveError(CommandInvalidError):
+ message = _("Dashboard cannot be unfavorited.")
diff --git a/superset/commands/dashboard/fave.py b/superset/commands/dashboard/fave.py
new file mode 100644
index 0000000000000..e3050c729dbec
--- /dev/null
+++ b/superset/commands/dashboard/fave.py
@@ -0,0 +1,46 @@
+# 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.
+import logging
+from functools import partial
+
+from requests_cache import Optional
+
+from superset.commands.base import BaseCommand
+from superset.commands.dashboard.exceptions import (
+ DashboardFaveError,
+)
+from superset.daos.dashboard import DashboardDAO
+from superset.models.dashboard import Dashboard
+from superset.utils.decorators import on_error, transaction
+
+logger = logging.getLogger(__name__)
+
+
+class AddFavoriteDashboardCommand(BaseCommand):
+ def __init__(self, dashboard_id: int) -> None:
+ self._dashboard_id = dashboard_id
+ self._dashboard: Optional[Dashboard] = None
+
+ @transaction(on_error=partial(on_error, reraise=DashboardFaveError))
+ def run(self) -> None:
+ self.validate()
+ return DashboardDAO.add_favorite(self._dashboard)
+
+ def validate(self) -> None:
+ # Raises DashboardNotFoundError or DashboardAccessDeniedError
+ dashboard = DashboardDAO.get_by_id_or_slug(self._dashboard_id)
+ self._dashboard = dashboard
diff --git a/superset/commands/dashboard/unfave.py b/superset/commands/dashboard/unfave.py
new file mode 100644
index 0000000000000..811a2cdd1e68d
--- /dev/null
+++ b/superset/commands/dashboard/unfave.py
@@ -0,0 +1,46 @@
+# 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.
+import logging
+from functools import partial
+
+from requests_cache import Optional
+
+from superset.commands.base import BaseCommand
+from superset.commands.dashboard.exceptions import (
+ DashboardUnfaveError,
+)
+from superset.daos.dashboard import DashboardDAO
+from superset.models.dashboard import Dashboard
+from superset.utils.decorators import on_error, transaction
+
+logger = logging.getLogger(__name__)
+
+
+class DelFavoriteDashboardCommand(BaseCommand):
+ def __init__(self, dashboard_id: int) -> None:
+ self._dashboard_id = dashboard_id
+ self._dashboard: Optional[Dashboard] = None
+
+ @transaction(on_error=partial(on_error, reraise=DashboardUnfaveError))
+ def run(self) -> None:
+ self.validate()
+ return DashboardDAO.remove_favorite(self._dashboard)
+
+ def validate(self) -> None:
+ # Raises DashboardNotFoundError or DashboardAccessDeniedError
+ dashboard = DashboardDAO.get_by_id_or_slug(self._dashboard_id)
+ self._dashboard = dashboard
diff --git a/superset/commands/database/exceptions.py b/superset/commands/database/exceptions.py
index 410eb9236b210..5285deb0f9dd8 100644
--- a/superset/commands/database/exceptions.py
+++ b/superset/commands/database/exceptions.py
@@ -158,7 +158,7 @@ class DatabaseTestConnectionUnexpectedError(SupersetErrorsException):
message = _("Unexpected error occurred, please check your logs for details")
-class DatabaseTablesUnexpectedError(Exception):
+class DatabaseTablesUnexpectedError(CommandException):
status = 422
message = _("Unexpected error occurred, please check your logs for details")
@@ -202,3 +202,15 @@ class DatabaseOfflineError(SupersetErrorException):
class InvalidParametersError(SupersetErrorsException):
status = 422
+
+
+class DatasetValidationError(CommandException):
+ status = 422
+
+ def __init__(self, err: Exception) -> None:
+ super().__init__(
+ _(
+ "Dataset schema is invalid, caused by: %(error)s",
+ error=str(err),
+ )
+ )
diff --git a/superset/commands/database/tables.py b/superset/commands/database/tables.py
index 80f174889846a..b28d4f065f626 100644
--- a/superset/commands/database/tables.py
+++ b/superset/commands/database/tables.py
@@ -130,7 +130,7 @@ def run(self) -> dict[str, Any]:
except SupersetException:
raise
except Exception as ex:
- raise DatabaseTablesUnexpectedError(ex) from ex
+ raise DatabaseTablesUnexpectedError(str(ex)) from ex
def validate(self) -> None:
self._model = cast(Database, DatabaseDAO.find_by_id(self._db_id))
diff --git a/superset/commands/sql_lab/execute.py b/superset/commands/sql_lab/execute.py
index 0c3e33b529169..001d5609db445 100644
--- a/superset/commands/sql_lab/execute.py
+++ b/superset/commands/sql_lab/execute.py
@@ -17,7 +17,6 @@
# pylint: disable=too-few-public-methods, too-many-arguments
from __future__ import annotations
-import copy
import logging
from typing import Any, TYPE_CHECKING
@@ -152,8 +151,6 @@ def _run_sql_json_exec_from_scratch(self) -> SqlJsonExecutionStatus:
self._validate_access(query)
self._execution_context.set_query(query)
rendered_query = self._sql_query_render.render(self._execution_context)
- validate_rendered_query = copy.copy(query)
- validate_rendered_query.sql = rendered_query
self._set_query_limit_if_required(rendered_query)
self._query_dao.update(
query, {"limit": self._execution_context.query.limit}
diff --git a/superset/config.py b/superset/config.py
index 670c518931078..42a46012e88d4 100644
--- a/superset/config.py
+++ b/superset/config.py
@@ -33,10 +33,11 @@
import re
import sys
from collections import OrderedDict
+from contextlib import contextmanager
from datetime import timedelta
from email.mime.multipart import MIMEMultipart
from importlib.resources import files
-from typing import Any, Callable, Literal, TYPE_CHECKING, TypedDict
+from typing import Any, Callable, Iterator, Literal, TYPE_CHECKING, TypedDict
import click
import pkg_resources
@@ -196,12 +197,14 @@ def _try_json_readsha(filepath: str, length: int) -> str | None:
# SQLALCHEMY_DATABASE_URI = 'mysql://myapp@localhost/myapp'
# SQLALCHEMY_DATABASE_URI = 'postgresql://root:password@localhost/myapp'
-# The default MySQL isolation level is REPEATABLE READ whereas the default PostgreSQL
-# isolation level is READ COMMITTED. All backends should use READ COMMITTED (or similar)
-# to help ensure consistent behavior.
-SQLALCHEMY_ENGINE_OPTIONS = {
- "isolation_level": "SERIALIZABLE", # SQLite does not support READ COMMITTED.
-}
+# This config is exposed through flask-sqlalchemy, and can be used to set your metadata
+# database connection settings. You can use this to set arbitrary connection settings
+# that may be specific to the database engine you are using.
+# Note that you can use this to set the isolation level of your database, as in
+# `SQLALCHEMY_ENGINE_OPTIONS = {"isolation_level": "READ COMMITTED"}`
+# Also note that we recommend READ COMMITTED for regular operation.
+# Find out more here https://flask-sqlalchemy.palletsprojects.com/en/3.1.x/config/
+SQLALCHEMY_ENGINE_OPTIONS = {}
# In order to hook up a custom password store for all SQLALCHEMY connections
# implement a function that takes a single argument of type 'sqla.engine.url',
@@ -543,6 +546,10 @@ class D3TimeFormat(TypedDict, total=False):
"SQLLAB_FORCE_RUN_ASYNC": False,
# Set to True to to enable factory resent CLI command
"ENABLE_FACTORY_RESET_COMMAND": False,
+ # Whether Superset should use Slack avatars for users.
+ # If on, you'll want to add "https://avatars.slack-edge.com" to the list of allowed
+ # domains in your TALISMAN_CONFIG
+ "SLACK_ENABLE_AVATARS": False,
}
# ------------------------------
@@ -1045,6 +1052,10 @@ class CeleryConfig: # pylint: disable=too-few-public-methods
# timeout.
SQLLAB_QUERY_COST_ESTIMATE_TIMEOUT = int(timedelta(seconds=10).total_seconds())
+# Timeout duration for SQL Lab fetching query results by the resultsKey.
+# 0 means no timeout.
+SQLLAB_QUERY_RESULT_TIMEOUT = 0
+
# The cost returned by the databases is a relative value; in order to map the cost to
# a tangible value you need to define a custom formatter that takes into consideration
# your specific infrastructure. For example, you could analyze queries a posteriori by
@@ -1136,16 +1147,18 @@ def CSV_TO_HIVE_UPLOAD_DIRECTORY_FUNC( # pylint: disable=invalid-name
# uploading CSVs will be stored.
UPLOADED_CSV_HIVE_NAMESPACE: str | None = None
+
# Function that computes the allowed schemas for the CSV uploads.
# Allowed schemas will be a union of schemas_allowed_for_file_upload
# db configuration and a result of this function.
+def allowed_schemas_for_csv_upload( # pylint: disable=unused-argument
+ database: Database,
+ user: models.User,
+) -> list[str]:
+ return [UPLOADED_CSV_HIVE_NAMESPACE] if UPLOADED_CSV_HIVE_NAMESPACE else []
-# mypy doesn't catch that if case ensures list content being always str
-ALLOWED_USER_CSV_SCHEMA_FUNC: Callable[[Database, models.User], list[str]] = ( # noqa: E731
- lambda database, user: [UPLOADED_CSV_HIVE_NAMESPACE]
- if UPLOADED_CSV_HIVE_NAMESPACE
- else []
-)
+
+ALLOWED_USER_CSV_SCHEMA_FUNC = allowed_schemas_for_csv_upload
# Values that should be treated as nulls for the csv uploads.
CSV_DEFAULT_NA_NAMES = list(STR_NA_VALUES)
@@ -1256,6 +1269,21 @@ def CSV_TO_HIVE_UPLOAD_DIRECTORY_FUNC( # pylint: disable=invalid-name
# The id of a template dashboard that should be copied to every new user
DASHBOARD_TEMPLATE_ID = None
+
+# A context manager that wraps the call to `create_engine`. This can be used for many
+# things, such as chrooting to prevent 3rd party drivers to access the filesystem, or
+# setting up custom configuration for database drivers.
+@contextmanager
+def engine_context_manager( # pylint: disable=unused-argument
+ database: Database,
+ catalog: str | None,
+ schema: str | None,
+) -> Iterator[None]:
+ yield None
+
+
+ENGINE_CONTEXT_MANAGER = engine_context_manager
+
# A callable that allows altering the database connection URL and params
# on the fly, at runtime. This allows for things like impersonation or
# arbitrary logic. For instance you can wire different users to
@@ -1421,11 +1449,6 @@ def EMAIL_HEADER_MUTATOR( # pylint: disable=invalid-name,unused-argument
SLACK_API_TOKEN: Callable[[], str] | str | None = None
SLACK_PROXY = None
-# Whether Superset should use Slack avatars for users.
-# If on, you'll want to add "https://avatars.slack-edge.com" to the list of allowed
-# domains in your TALISMAN_CONFIG
-SLACK_ENABLE_AVATARS = False
-
# The webdriver to use for generating reports. Use one of the following
# firefox
# Requires: geckodriver and firefox installations
@@ -1721,6 +1744,15 @@ def EMAIL_HEADER_MUTATOR( # pylint: disable=invalid-name,unused-argument
# Guest token audience for the embedded superset, either string or callable
GUEST_TOKEN_JWT_AUDIENCE: Callable[[], str] | str | None = None
+# A callable that can be supplied to do extra validation of guest token configuration
+# for example certain RLS parameters:
+# lambda x: len(x['rls']) == 1 and "tenant_id=" in x['rls'][0]['clause']
+#
+# Takes the GuestTokenUser dict as an argument
+# Return False from the callable to return a HTTP 400 to the user.
+
+GUEST_TOKEN_VALIDATOR_HOOK = None
+
# A SQL dataset health check. Note if enabled it is strongly advised that the callable
# be memoized to aid with performance, i.e.,
#
diff --git a/superset/connectors/sqla/models.py b/superset/connectors/sqla/models.py
index a70b860d2ee84..9215e1545bb21 100644
--- a/superset/connectors/sqla/models.py
+++ b/superset/connectors/sqla/models.py
@@ -83,7 +83,6 @@
from superset.exceptions import (
ColumnNotFoundException,
DatasetInvalidPermissionEvaluationException,
- QueryClauseValidationException,
QueryObjectValidationError,
SupersetErrorException,
SupersetErrorsException,
@@ -103,10 +102,9 @@
ExploreMixin,
ImportExportMixin,
QueryResult,
- validate_adhoc_subquery,
)
from superset.models.slice import Slice
-from superset.sql_parse import ParsedQuery, sanitize_clause, Table
+from superset.sql_parse import ParsedQuery, Table
from superset.superset_typing import (
AdhocColumn,
AdhocMetric,
@@ -1136,27 +1134,6 @@ def data(self) -> dict[str, Any]:
)
-def _process_sql_expression(
- expression: str | None,
- database_id: int,
- schema: str,
- template_processor: BaseTemplateProcessor | None = None,
-) -> str | None:
- if template_processor and expression:
- expression = template_processor.process_template(expression)
- if expression:
- try:
- expression = validate_adhoc_subquery(
- expression,
- database_id,
- schema,
- )
- expression = sanitize_clause(expression)
- except (QueryClauseValidationException, SupersetSecurityException) as ex:
- raise QueryObjectValidationError(ex.message) from ex
- return expression
-
-
class SqlaTable(
Model,
BaseDatasource,
@@ -1552,12 +1529,15 @@ def adhoc_metric_to_sqla(
sqla_column = column(column_name)
sqla_metric = self.sqla_aggregations[metric["aggregate"]](sqla_column)
elif expression_type == utils.AdhocMetricExpressionType.SQL:
- expression = _process_sql_expression(
- expression=metric["sqlExpression"],
- database_id=self.database_id,
- schema=self.schema,
- template_processor=template_processor,
- )
+ try:
+ expression = self._process_sql_expression(
+ expression=metric["sqlExpression"],
+ database_id=self.database_id,
+ schema=self.schema,
+ template_processor=template_processor,
+ )
+ except SupersetSecurityException as ex:
+ raise QueryObjectValidationError(ex.message) from ex
sqla_metric = literal_column(expression)
else:
raise QueryObjectValidationError("Adhoc metric expressionType is invalid")
@@ -1582,12 +1562,15 @@ def adhoc_column_to_sqla( # pylint: disable=too-many-locals
:rtype: sqlalchemy.sql.column
"""
label = utils.get_column_name(col)
- expression = _process_sql_expression(
- expression=col["sqlExpression"],
- database_id=self.database_id,
- schema=self.schema,
- template_processor=template_processor,
- )
+ try:
+ expression = self._process_sql_expression(
+ expression=col["sqlExpression"],
+ database_id=self.database_id,
+ schema=self.schema,
+ template_processor=template_processor,
+ )
+ except SupersetSecurityException as ex:
+ raise QueryObjectValidationError(ex.message) from ex
time_grain = col.get("timeGrain")
has_timegrain = col.get("columnType") == "BASE_AXIS" and time_grain
is_dttm = False
@@ -1705,10 +1688,10 @@ def _normalize_prequery_result_type(
if isinstance(value, np.generic):
value = value.item()
- column_ = columns_by_name[dimension]
+ column_ = columns_by_name.get(dimension)
db_extra: dict[str, Any] = self.database.get_extra()
- if column_.type and column_.is_temporal and isinstance(value, str):
+ if column_ and column_.type and column_.is_temporal and isinstance(value, str):
sql = self.db_engine_spec.convert_dttm(
column_.type, dateutil.parser.parse(value), db_extra=db_extra
)
diff --git a/superset/dashboards/api.py b/superset/dashboards/api.py
index 53371925a925a..cb7e30ef02838 100644
--- a/superset/dashboards/api.py
+++ b/superset/dashboards/api.py
@@ -22,7 +22,7 @@
from typing import Any, Callable, cast, Optional
from zipfile import is_zipfile, ZipFile
-from flask import redirect, request, Response, send_file, url_for
+from flask import g, redirect, request, Response, send_file, url_for
from flask_appbuilder import permission_name
from flask_appbuilder.api import expose, protect, rison, safe
from flask_appbuilder.hooks import before_request
@@ -51,9 +51,12 @@
DashboardUpdateFailedError,
)
from superset.commands.dashboard.export import ExportDashboardsCommand
+from superset.commands.dashboard.fave import AddFavoriteDashboardCommand
from superset.commands.dashboard.importers.dispatcher import ImportDashboardsCommand
from superset.commands.dashboard.permalink.create import CreateDashboardPermalinkCommand
+from superset.commands.dashboard.unfave import DelFavoriteDashboardCommand
from superset.commands.dashboard.update import UpdateDashboardCommand
+from superset.commands.database.exceptions import DatasetValidationError
from superset.commands.exceptions import TagForbiddenError
from superset.commands.importers.exceptions import NoValidFilesFoundError
from superset.commands.importers.v1.utils import get_contents_from_bundle
@@ -93,6 +96,7 @@
from superset.extensions import event_logger
from superset.models.dashboard import Dashboard
from superset.models.embedded_dashboard import EmbeddedDashboard
+from superset.security.guest_token import GuestUser
from superset.tasks.thumbnails import (
cache_dashboard_screenshot,
cache_dashboard_thumbnail,
@@ -112,6 +116,7 @@
requires_json,
statsd_metrics,
)
+from superset.views.error_handling import handle_api_exception
from superset.views.filters import (
BaseFilterRelatedRoles,
BaseFilterRelatedUsers,
@@ -366,7 +371,7 @@ def get(
@expose("//datasets", methods=("GET",))
@protect()
- @safe
+ @handle_api_exception
@statsd_metrics
@event_logger.log_this_with_context(
action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.get_datasets",
@@ -414,15 +419,7 @@ def get_datasets(self, id_or_slug: str) -> Response:
]
return self.response(200, result=result)
except (TypeError, ValueError) as err:
- return self.response_400(
- message=gettext(
- "Dataset schema is invalid, caused by: %(error)s", error=str(err)
- )
- )
- except DashboardAccessDeniedError:
- return self.response_403()
- except DashboardNotFoundError:
- return self.response_404()
+ raise DatasetValidationError(err) from err
@expose("//tabs", methods=("GET",))
@protect()
@@ -1033,7 +1030,7 @@ def cache_dashboard_screenshot(self, pk: int, **kwargs: Any) -> WerkzeugResponse
dashboard_url = get_url_path("Superset.dashboard_permalink", key=permalink_key)
screenshot_obj = DashboardScreenshot(dashboard_url, dashboard.digest)
- cache_key = screenshot_obj.cache_key(window_size, thumb_size)
+ cache_key = screenshot_obj.cache_key(window_size, thumb_size, dashboard_state)
image_url = get_url_path(
"DashboardRestApi.screenshot", pk=dashboard.id, digest=cache_key
)
@@ -1041,9 +1038,13 @@ def cache_dashboard_screenshot(self, pk: int, **kwargs: Any) -> WerkzeugResponse
def trigger_celery() -> WerkzeugResponse:
logger.info("Triggering screenshot ASYNC")
cache_dashboard_screenshot.delay(
- current_user=get_current_user(),
+ username=get_current_user(),
+ guest_token=g.user.guest_token
+ if get_current_user() and isinstance(g.user, GuestUser)
+ else None,
dashboard_id=dashboard.id,
dashboard_url=dashboard_url,
+ cache_key=cache_key,
force=True,
thumb_size=thumb_size,
window_size=window_size,
@@ -1213,11 +1214,14 @@ def add_favorite(self, pk: int) -> Response:
500:
$ref: '#/components/responses/500'
"""
- dashboard = DashboardDAO.find_by_id(pk)
- if not dashboard:
+ try:
+ AddFavoriteDashboardCommand(pk).run()
+
+ except DashboardNotFoundError:
return self.response_404()
+ except DashboardAccessDeniedError:
+ return self.response_403()
- DashboardDAO.add_favorite(dashboard)
return self.response(200, result="OK")
@expose("//favorites/", methods=("DELETE",))
@@ -1256,11 +1260,13 @@ def remove_favorite(self, pk: int) -> Response:
500:
$ref: '#/components/responses/500'
"""
- dashboard = DashboardDAO.find_by_id(pk)
- if not dashboard:
+ try:
+ DelFavoriteDashboardCommand(pk).run()
+ except DashboardNotFoundError:
return self.response_404()
+ except DashboardAccessDeniedError:
+ return self.response_403()
- DashboardDAO.remove_favorite(dashboard)
return self.response(200, result="OK")
@expose("/import/", methods=("POST",))
diff --git a/superset/databases/api.py b/superset/databases/api.py
index b58e46bf3fcbe..88188bed57b90 100644
--- a/superset/databases/api.py
+++ b/superset/databases/api.py
@@ -41,7 +41,6 @@
DatabaseDeleteFailedError,
DatabaseInvalidError,
DatabaseNotFoundError,
- DatabaseTablesUnexpectedError,
DatabaseUpdateFailedError,
InvalidParametersError,
)
@@ -131,7 +130,7 @@
requires_json,
statsd_metrics,
)
-from superset.views.error_handling import json_error_response
+from superset.views.error_handling import handle_api_exception, json_error_response
from superset.views.filters import BaseFilterRelatedUsers, FilterRelatedOwners
logger = logging.getLogger(__name__)
@@ -755,9 +754,9 @@ def schemas(self, pk: int, **kwargs: Any) -> FlaskResponse:
@expose("//tables/")
@protect()
- @safe
@rison(database_tables_query_schema)
@statsd_metrics
+ @handle_api_exception
@event_logger.log_this_with_context(
action=lambda self, *args, **kwargs: f"{self.__class__.__name__}" f".tables",
log_to_statsd=False,
@@ -810,16 +809,9 @@ def tables(self, pk: int, **kwargs: Any) -> FlaskResponse:
catalog_name = kwargs["rison"].get("catalog_name")
schema_name = kwargs["rison"].get("schema_name", "")
- try:
- command = TablesDatabaseCommand(pk, catalog_name, schema_name, force)
- payload = command.run()
- return self.response(200, **payload)
- except DatabaseNotFoundError:
- return self.response_404()
- except SupersetException as ex:
- return self.response(ex.status, message=ex.message)
- except DatabaseTablesUnexpectedError as ex:
- return self.response_422(ex.message)
+ command = TablesDatabaseCommand(pk, catalog_name, schema_name, force)
+ payload = command.run()
+ return self.response(200, **payload)
@expose("//table///", methods=("GET",))
@protect()
diff --git a/superset/db_engine_specs/base.py b/superset/db_engine_specs/base.py
index 2b32d156df4d7..dcdfff6c3f242 100644
--- a/superset/db_engine_specs/base.py
+++ b/superset/db_engine_specs/base.py
@@ -63,7 +63,8 @@
from superset.databases.utils import get_table_metadata, make_url_safe
from superset.errors import ErrorLevel, SupersetError, SupersetErrorType
from superset.exceptions import DisallowedSQLFunction, OAuth2Error, OAuth2RedirectError
-from superset.sql_parse import ParsedQuery, SQLScript, Table
+from superset.sql.parse import SQLScript, Table
+from superset.sql_parse import ParsedQuery
from superset.superset_typing import (
OAuth2ClientConfig,
OAuth2State,
@@ -347,6 +348,7 @@ class BaseEngineSpec: # pylint: disable=too-many-public-methods
# Does database support join-free timeslot grouping
time_groupby_inline = False
limit_method = LimitMethod.FORCE_LIMIT
+ supports_multivalues_insert = False
allows_joins = True
allows_subqueries = True
allows_alias_in_select = True
@@ -1283,9 +1285,11 @@ def df_to_sql(
catalog=table.catalog,
schema=table.schema,
) as engine:
- if engine.dialect.supports_multivalues_insert:
+ if (
+ engine.dialect.supports_multivalues_insert
+ or cls.supports_multivalues_insert
+ ):
to_sql_kwargs["method"] = "multi"
-
df.to_sql(con=engine, **to_sql_kwargs)
@classmethod
diff --git a/superset/db_engine_specs/dremio.py b/superset/db_engine_specs/dremio.py
index 32ffee81a6c5c..1f4338830bb8f 100644
--- a/superset/db_engine_specs/dremio.py
+++ b/superset/db_engine_specs/dremio.py
@@ -38,6 +38,11 @@ class DremioEngineSpec(BaseEngineSpec):
engine = "dremio"
engine_name = "Dremio"
engine_aliases = {"dremio+flight"}
+ drivers = {
+ "flight": "Arrow Flight driver for Dremio",
+ "pyodbc": "ODBC driver for Dremio",
+ }
+ default_driver = "flight"
sqlalchemy_uri_placeholder = (
"dremio+flight://data.dremio.cloud:443/?"
"Token=&"
diff --git a/superset/db_engine_specs/mssql.py b/superset/db_engine_specs/mssql.py
index d5cc86c859a7b..464f6cf2b9c8d 100644
--- a/superset/db_engine_specs/mssql.py
+++ b/superset/db_engine_specs/mssql.py
@@ -53,6 +53,7 @@ class MssqlEngineSpec(BaseEngineSpec):
max_column_name_length = 128
allows_cte_in_subquery = False
allow_limit_clause = False
+ supports_multivalues_insert = True
_time_grain_expressions = {
None: "{col}",
diff --git a/superset/db_engine_specs/postgres.py b/superset/db_engine_specs/postgres.py
index 8525ea05da9b6..70373927d521b 100644
--- a/superset/db_engine_specs/postgres.py
+++ b/superset/db_engine_specs/postgres.py
@@ -35,7 +35,7 @@
from superset.errors import ErrorLevel, SupersetError, SupersetErrorType
from superset.exceptions import SupersetException, SupersetSecurityException
from superset.models.sql_lab import Query
-from superset.sql_parse import SQLScript
+from superset.sql.parse import SQLScript
from superset.utils import core as utils, json
from superset.utils.core import GenericDataType
@@ -103,7 +103,13 @@ class PostgresBaseEngineSpec(BaseEngineSpec):
_time_grain_expressions = {
None: "{col}",
TimeGrain.SECOND: "DATE_TRUNC('second', {col})",
+ TimeGrain.FIVE_SECONDS: "DATE_TRUNC('minute', {col}) + INTERVAL '5 seconds' * FLOOR(EXTRACT(SECOND FROM {col}) / 5)",
+ TimeGrain.THIRTY_SECONDS: "DATE_TRUNC('minute', {col}) + INTERVAL '30 seconds' * FLOOR(EXTRACT(SECOND FROM {col}) / 30)",
TimeGrain.MINUTE: "DATE_TRUNC('minute', {col})",
+ TimeGrain.FIVE_MINUTES: "DATE_TRUNC('hour', {col}) + INTERVAL '5 minutes' * FLOOR(EXTRACT(MINUTE FROM {col}) / 5)",
+ TimeGrain.TEN_MINUTES: "DATE_TRUNC('hour', {col}) + INTERVAL '10 minutes' * FLOOR(EXTRACT(MINUTE FROM {col}) / 10)",
+ TimeGrain.FIFTEEN_MINUTES: "DATE_TRUNC('hour', {col}) + INTERVAL '15 minutes' * FLOOR(EXTRACT(MINUTE FROM {col}) / 15)",
+ TimeGrain.THIRTY_MINUTES: "DATE_TRUNC('hour', {col}) + INTERVAL '30 minutes' * FLOOR(EXTRACT(MINUTE FROM {col}) / 30)",
TimeGrain.HOUR: "DATE_TRUNC('hour', {col})",
TimeGrain.DAY: "DATE_TRUNC('day', {col})",
TimeGrain.WEEK: "DATE_TRUNC('week', {col})",
diff --git a/superset/exceptions.py b/superset/exceptions.py
index a000e08165c47..492007523de3f 100644
--- a/superset/exceptions.py
+++ b/superset/exceptions.py
@@ -14,6 +14,9 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
+
+from __future__ import annotations
+
from collections import defaultdict
from typing import Any, Optional
@@ -304,12 +307,30 @@ class SupersetParseError(SupersetErrorException):
status = 422
- def __init__(self, sql: str, engine: Optional[str] = None):
+ def __init__( # pylint: disable=too-many-arguments
+ self,
+ sql: str,
+ engine: Optional[str] = None,
+ message: Optional[str] = None,
+ highlight: Optional[str] = None,
+ line: Optional[int] = None,
+ column: Optional[int] = None,
+ ):
+ if message is None:
+ parts = [_("Error parsing")]
+ if highlight:
+ parts.append(_(" near '%(highlight)s'", highlight=highlight))
+ if line:
+ parts.append(_(" at line %(line)d", line=line))
+ if column:
+ parts.append(_(":%(column)d", column=column))
+ message = "".join(parts)
+
error = SupersetError(
- message=_("The SQL is invalid and cannot be parsed."),
+ message=message,
error_type=SupersetErrorType.INVALID_SQL_ERROR,
level=ErrorLevel.ERROR,
- extra={"sql": sql, "engine": engine},
+ extra={"sql": sql, "engine": engine, "line": line, "column": column},
)
super().__init__(error)
diff --git a/superset/extensions/metadb.py b/superset/extensions/metadb.py
index fd697aea820f3..b2d86149383cb 100644
--- a/superset/extensions/metadb.py
+++ b/superset/extensions/metadb.py
@@ -274,7 +274,7 @@ def __init__(
# to perform updates and deletes. Otherwise we can only do inserts and selects.
self._rowid: str | None = None
- # Does the database allow DML?
+ # Does the database allow DDL/DML?
self._allow_dml: bool = False
# Read column information from the database, and store it for later.
diff --git a/superset/initialization/__init__.py b/superset/initialization/__init__.py
index 2601cda9d7da7..10686b872ff41 100644
--- a/superset/initialization/__init__.py
+++ b/superset/initialization/__init__.py
@@ -32,6 +32,7 @@
from werkzeug.middleware.proxy_fix import ProxyFix
from superset.constants import CHANGE_ME_SECRET_KEY
+from superset.databases.utils import make_url_safe
from superset.extensions import (
_event_logger,
APP_DIR,
@@ -482,12 +483,36 @@ def init_app(self) -> None:
self.configure_wtf()
self.configure_middlewares()
self.configure_cache()
+ self.set_db_default_isolation()
with self.superset_app.app_context():
self.init_app_in_ctx()
self.post_init()
+ def set_db_default_isolation(self) -> None:
+ # This block sets the default isolation level for mysql to READ COMMITTED if not
+ # specified in the config. You can set your isolation in the config by using
+ # SQLALCHEMY_ENGINE_OPTIONS
+ eng_options = self.config["SQLALCHEMY_ENGINE_OPTIONS"] or {}
+ isolation_level = eng_options.get("isolation_level")
+ set_isolation_level_to = None
+
+ if not isolation_level:
+ backend = make_url_safe(
+ self.config["SQLALCHEMY_DATABASE_URI"]
+ ).get_backend_name()
+ if backend in ("mysql", "postgresql"):
+ set_isolation_level_to = "READ COMMITTED"
+
+ if set_isolation_level_to:
+ logger.info(
+ "Setting database isolation level to %s",
+ set_isolation_level_to,
+ )
+ with self.superset_app.app_context():
+ db.engine.execution_options(isolation_level=set_isolation_level_to)
+
def configure_auth_provider(self) -> None:
machine_auth_provider_factory.init_app(self.superset_app)
diff --git a/superset/jinja_context.py b/superset/jinja_context.py
index 625e59fe6ae38..d7ae892301689 100644
--- a/superset/jinja_context.py
+++ b/superset/jinja_context.py
@@ -375,11 +375,13 @@ def get_filters(self, column: str, remove_filter: bool = False) -> list[Filter]:
return filters
+ # pylint: disable=too-many-arguments
def get_time_filter(
self,
column: str | None = None,
default: str | None = None,
target_type: str | None = None,
+ strftime: str | None = None,
remove_filter: bool = False,
) -> TimeFilter:
"""Get the time filter with appropriate formatting,
@@ -395,6 +397,8 @@ def get_time_filter(
the format will default to the type of the column. This is used to produce
the format of the `from_expr` and `to_expr` properties of the returned
`TimeFilter` object.
+ :param strftime: format using the `strftime` method of `datetime`. When defined
+ `target_type` will be ignored.
:param remove_filter: When set to true, mark the filter as processed,
removing it from the outer query. Useful when a filter should
only apply to the inner query.
@@ -434,6 +438,8 @@ def get_time_filter(
from_expr, to_expr = get_since_until_from_time_range(time_range)
def _format_dttm(dttm: datetime | None) -> str | None:
+ if strftime and dttm:
+ return dttm.strftime(strftime)
return (
self.database.db_engine_spec.convert_dttm(target_type or "", dttm)
if self.database and dttm
diff --git a/superset/migrations/versions/2024-02-07_17-13_87d38ad83218_migrate_can_view_and_drill_permission.py b/superset/migrations/versions/2024-02-07_17-13_87d38ad83218_migrate_can_view_and_drill_permission.py
index 175822cc25e99..1fc4158357dbd 100644
--- a/superset/migrations/versions/2024-02-07_17-13_87d38ad83218_migrate_can_view_and_drill_permission.py
+++ b/superset/migrations/versions/2024-02-07_17-13_87d38ad83218_migrate_can_view_and_drill_permission.py
@@ -38,7 +38,7 @@
Pvm,
)
-NEW_PVMS = {"Dashboard": ("can_view_chart_as_table",)}
+NEW_PVMS = {"Dashboard": ("can_view_chart_as_table", "can_view_query")}
PVM_MAP = {
Pvm("Dashboard", "can_view_and_drill"): (
diff --git a/superset/models/core.py b/superset/models/core.py
index 9d432f8114c6a..5d3a6ea74ddab 100755
--- a/superset/models/core.py
+++ b/superset/models/core.py
@@ -418,38 +418,40 @@ def get_sqla_engine( # pylint: disable=too-many-arguments
)
sqlalchemy_uri = self.sqlalchemy_uri_decrypted
- engine_context = nullcontext()
- ssh_tunnel = override_ssh_tunnel or DatabaseDAO.get_ssh_tunnel(
- database_id=self.id
- )
- if ssh_tunnel:
- # if ssh_tunnel is available build engine with information
- engine_context = ssh_manager_factory.instance.create_tunnel(
+ ssh_tunnel = override_ssh_tunnel or DatabaseDAO.get_ssh_tunnel(self.id)
+ ssh_context_manager = (
+ ssh_manager_factory.instance.create_tunnel(
ssh_tunnel=ssh_tunnel,
sqlalchemy_database_uri=sqlalchemy_uri,
)
+ if ssh_tunnel
+ else nullcontext()
+ )
- with engine_context as server_context:
- if ssh_tunnel and server_context:
+ with ssh_context_manager as ssh_context:
+ if ssh_context:
logger.info(
- "[SSH] Successfully created tunnel w/ %s tunnel_timeout + %s ssh_timeout at %s",
+ "[SSH] Successfully created tunnel w/ %s tunnel_timeout + %s "
+ "ssh_timeout at %s",
sshtunnel.TUNNEL_TIMEOUT,
sshtunnel.SSH_TIMEOUT,
- server_context.local_bind_address,
+ ssh_context.local_bind_address,
)
sqlalchemy_uri = ssh_manager_factory.instance.build_sqla_url(
sqlalchemy_uri,
- server_context,
+ ssh_context,
)
- yield self._get_sqla_engine(
- catalog=catalog,
- schema=schema,
- nullpool=nullpool,
- source=source,
- sqlalchemy_uri=sqlalchemy_uri,
- )
+ engine_context_manager = config["ENGINE_CONTEXT_MANAGER"]
+ with engine_context_manager(self, catalog, schema):
+ yield self._get_sqla_engine(
+ catalog=catalog,
+ schema=schema,
+ nullpool=nullpool,
+ source=source,
+ sqlalchemy_uri=sqlalchemy_uri,
+ )
def _get_sqla_engine( # pylint: disable=too-many-locals
self,
diff --git a/superset/models/helpers.py b/superset/models/helpers.py
index 295ecea70ea45..80e66f50270c8 100644
--- a/superset/models/helpers.py
+++ b/superset/models/helpers.py
@@ -68,13 +68,12 @@
)
from superset.extensions import feature_flag_manager
from superset.jinja_context import BaseTemplateProcessor
+from superset.sql.parse import SQLScript, SQLStatement
from superset.sql_parse import (
has_table_query,
insert_rls_in_predicate,
ParsedQuery,
sanitize_clause,
- SQLScript,
- SQLStatement,
)
from superset.superset_typing import (
AdhocMetric,
diff --git a/superset/security/api.py b/superset/security/api.py
index 61fd68e6f0e7c..02bf6b7101ea8 100644
--- a/superset/security/api.py
+++ b/superset/security/api.py
@@ -17,7 +17,7 @@
import logging
from typing import Any
-from flask import request, Response
+from flask import current_app, request, Response
from flask_appbuilder import expose
from flask_appbuilder.api import safe
from flask_appbuilder.security.decorators import permission_name, protect
@@ -27,6 +27,7 @@
from superset.commands.dashboard.embedded.exceptions import (
EmbeddedDashboardNotFoundError,
)
+from superset.exceptions import SupersetGenericErrorException
from superset.extensions import event_logger
from superset.security.guest_token import GuestTokenResourceType
from superset.views.base_api import BaseSupersetApi, statsd_metrics
@@ -148,8 +149,19 @@ def guest_token(self) -> Response:
try:
body = guest_token_create_schema.load(request.json)
self.appbuilder.sm.validate_guest_token_resources(body["resources"])
-
- # todo validate stuff:
+ guest_token_validator_hook = current_app.config.get(
+ "GUEST_TOKEN_VALIDATOR_HOOK"
+ )
+ # Run validator to ensure the token parameters are OK.
+ if guest_token_validator_hook is not None:
+ if callable(guest_token_validator_hook):
+ if not guest_token_validator_hook(body):
+ raise ValidationError(message="Guest token validation failed")
+ else:
+ raise SupersetGenericErrorException(
+ message="Guest token validator hook not callable"
+ )
+ # TODO: Add generic validation:
# make sure username doesn't reference an existing user
# check rls rules for validity?
token = self.appbuilder.sm.create_guest_access_token(
diff --git a/superset/security/manager.py b/superset/security/manager.py
index 5ee540b643f0c..27484d6a8cfa2 100644
--- a/superset/security/manager.py
+++ b/superset/security/manager.py
@@ -67,6 +67,7 @@
GuestUser,
)
from superset.sql_parse import extract_tables_from_jinja_sql, Table
+from superset.tasks.utils import get_current_user
from superset.utils import json
from superset.utils.core import (
DatasourceName,
@@ -2639,8 +2640,12 @@ def is_guest_user(user: Optional[Any] = None) -> bool:
if not is_feature_enabled("EMBEDDED_SUPERSET"):
return False
+
if not user:
+ if not get_current_user():
+ return False
user = g.user
+
return hasattr(user, "is_guest_user") and user.is_guest_user
def get_current_guest_user_if_guest(self) -> Optional[GuestUser]:
diff --git a/superset/sql/__init__.py b/superset/sql/__init__.py
new file mode 100644
index 0000000000000..13a83393a9124
--- /dev/null
+++ b/superset/sql/__init__.py
@@ -0,0 +1,16 @@
+# 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.
diff --git a/superset/sql/parse.py b/superset/sql/parse.py
new file mode 100644
index 0000000000000..377411b944814
--- /dev/null
+++ b/superset/sql/parse.py
@@ -0,0 +1,648 @@
+# 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.
+
+from __future__ import annotations
+
+import enum
+import logging
+import re
+import urllib.parse
+from collections.abc import Iterable
+from dataclasses import dataclass
+from typing import Any, Generic, TypeVar
+
+import sqlglot
+from sqlglot import exp
+from sqlglot.dialects.dialect import Dialect, Dialects
+from sqlglot.errors import ParseError
+from sqlglot.optimizer.scope import Scope, ScopeType, traverse_scope
+
+from superset.exceptions import SupersetParseError
+
+logger = logging.getLogger(__name__)
+
+
+# mapping between DB engine specs and sqlglot dialects
+SQLGLOT_DIALECTS = {
+ "base": Dialects.DIALECT,
+ "ascend": Dialects.HIVE,
+ "awsathena": Dialects.PRESTO,
+ "bigquery": Dialects.BIGQUERY,
+ "clickhouse": Dialects.CLICKHOUSE,
+ "clickhousedb": Dialects.CLICKHOUSE,
+ "cockroachdb": Dialects.POSTGRES,
+ "couchbase": Dialects.MYSQL,
+ # "crate": ???
+ # "databend": ???
+ "databricks": Dialects.DATABRICKS,
+ # "db2": ???
+ # "dremio": ???
+ "drill": Dialects.DRILL,
+ # "druid": ???
+ "duckdb": Dialects.DUCKDB,
+ # "dynamodb": ???
+ # "elasticsearch": ???
+ # "exa": ???
+ # "firebird": ???
+ # "firebolt": ???
+ "gsheets": Dialects.SQLITE,
+ "hana": Dialects.POSTGRES,
+ "hive": Dialects.HIVE,
+ # "ibmi": ???
+ # "impala": ???
+ # "kustokql": ???
+ # "kylin": ???
+ "mssql": Dialects.TSQL,
+ "mysql": Dialects.MYSQL,
+ "netezza": Dialects.POSTGRES,
+ # "ocient": ???
+ # "odelasticsearch": ???
+ "oracle": Dialects.ORACLE,
+ # "pinot": ???
+ "postgresql": Dialects.POSTGRES,
+ "presto": Dialects.PRESTO,
+ "pydoris": Dialects.DORIS,
+ "redshift": Dialects.REDSHIFT,
+ # "risingwave": ???
+ # "rockset": ???
+ "shillelagh": Dialects.SQLITE,
+ "snowflake": Dialects.SNOWFLAKE,
+ # "solr": ???
+ "spark": Dialects.SPARK,
+ "sqlite": Dialects.SQLITE,
+ "starrocks": Dialects.STARROCKS,
+ "superset": Dialects.SQLITE,
+ "teradatasql": Dialects.TERADATA,
+ "trino": Dialects.TRINO,
+ "vertica": Dialects.POSTGRES,
+}
+
+
+@dataclass(eq=True, frozen=True)
+class Table:
+ """
+ A fully qualified SQL table conforming to [[catalog.]schema.]table.
+ """
+
+ table: str
+ schema: str | None = None
+ catalog: str | None = None
+
+ def __str__(self) -> str:
+ """
+ Return the fully qualified SQL table name.
+
+ Should not be used for SQL generation, only for logging and debugging, since the
+ quoting is not engine-specific.
+ """
+ return ".".join(
+ urllib.parse.quote(part, safe="").replace(".", "%2E")
+ for part in [self.catalog, self.schema, self.table]
+ if part
+ )
+
+ def __eq__(self, other: Any) -> bool:
+ return str(self) == str(other)
+
+
+# To avoid unnecessary parsing/formatting of queries, the statement has the concept of
+# an "internal representation", which is the AST of the SQL statement. For most of the
+# engines supported by Superset this is `sqlglot.exp.Expression`, but there is a special
+# case: KustoKQL uses a different syntax and there are no Python parsers for it, so we
+# store the AST as a string (the original query), and manipulate it with regular
+# expressions.
+InternalRepresentation = TypeVar("InternalRepresentation")
+
+# The base type. This helps type checking the `split_query` method correctly, since each
+# derived class has a more specific return type (the class itself). This will no longer
+# be needed once Python 3.11 is the lowest version supported. See PEP 673 for more
+# information: https://peps.python.org/pep-0673/
+TBaseSQLStatement = TypeVar("TBaseSQLStatement") # pylint: disable=invalid-name
+
+
+class BaseSQLStatement(Generic[InternalRepresentation]):
+ """
+ Base class for SQL statements.
+
+ The class can be instantiated with a string representation of the script or, for
+ efficiency reasons, with a pre-parsed AST. This is useful with `sqlglot.parse`,
+ which will split a script in multiple already parsed statements.
+
+ The `engine` parameters comes from the `engine` attribute in a Superset DB engine
+ spec.
+ """
+
+ def __init__(
+ self,
+ statement: str | InternalRepresentation,
+ engine: str,
+ ):
+ self._parsed: InternalRepresentation = (
+ self._parse_statement(statement, engine)
+ if isinstance(statement, str)
+ else statement
+ )
+ self.engine = engine
+ self.tables = self._extract_tables_from_statement(self._parsed, self.engine)
+
+ @classmethod
+ def split_script(
+ cls: type[TBaseSQLStatement],
+ script: str,
+ engine: str,
+ ) -> list[TBaseSQLStatement]:
+ """
+ Split a script into multiple instantiated statements.
+
+ This is a helper function to split a full SQL script into multiple
+ `BaseSQLStatement` instances. It's used by `SQLScript` when instantiating the
+ statements within a script.
+ """
+ raise NotImplementedError()
+
+ @classmethod
+ def _parse_statement(
+ cls,
+ statement: str,
+ engine: str,
+ ) -> InternalRepresentation:
+ """
+ Parse a string containing a single SQL statement, and returns the parsed AST.
+
+ Derived classes should not assume that `statement` contains a single statement,
+ and MUST explicitly validate that. Since this validation is parser dependent the
+ responsibility is left to the children classes.
+ """
+ raise NotImplementedError()
+
+ @classmethod
+ def _extract_tables_from_statement(
+ cls,
+ parsed: InternalRepresentation,
+ engine: str,
+ ) -> set[Table]:
+ """
+ Extract all table references in a given statement.
+ """
+ raise NotImplementedError()
+
+ def format(self, comments: bool = True) -> str:
+ """
+ Format the statement, optionally ommitting comments.
+ """
+ raise NotImplementedError()
+
+ def get_settings(self) -> dict[str, str | bool]:
+ """
+ Return any settings set by the statement.
+
+ For example, for this statement:
+
+ sql> SET foo = 'bar';
+
+ The method should return `{"foo": "'bar'"}`. Note the single quotes.
+ """
+ raise NotImplementedError()
+
+ def is_mutating(self) -> bool:
+ """
+ Check if the statement mutates data (DDL/DML).
+
+ :return: True if the statement mutates data.
+ """
+ raise NotImplementedError()
+
+ def __str__(self) -> str:
+ return self.format()
+
+
+class SQLStatement(BaseSQLStatement[exp.Expression]):
+ """
+ A SQL statement.
+
+ This class is used for all engines with dialects that can be parsed using sqlglot.
+ """
+
+ def __init__(
+ self,
+ statement: str | exp.Expression,
+ engine: str,
+ ):
+ self._dialect = SQLGLOT_DIALECTS.get(engine)
+ super().__init__(statement, engine)
+
+ @classmethod
+ def _parse(cls, script: str, engine: str) -> list[exp.Expression]:
+ """
+ Parse helper.
+ """
+ dialect = SQLGLOT_DIALECTS.get(engine)
+ try:
+ return sqlglot.parse(script, dialect=dialect)
+ except sqlglot.errors.ParseError as ex:
+ error = ex.errors[0]
+ raise SupersetParseError(
+ script,
+ engine,
+ highlight=error["highlight"],
+ line=error["line"],
+ column=error["col"],
+ ) from ex
+ except sqlglot.errors.SqlglotError as ex:
+ raise SupersetParseError(
+ script,
+ engine,
+ message="Unable to parse script",
+ ) from ex
+
+ @classmethod
+ def split_script(
+ cls,
+ script: str,
+ engine: str,
+ ) -> list[SQLStatement]:
+ return [
+ cls(statement, engine)
+ for statement in cls._parse(script, engine)
+ if statement
+ ]
+
+ @classmethod
+ def _parse_statement(
+ cls,
+ statement: str,
+ engine: str,
+ ) -> exp.Expression:
+ """
+ Parse a single SQL statement.
+ """
+ statements = cls.split_script(statement, engine)
+ if len(statements) != 1:
+ raise SupersetParseError("SQLStatement should have exactly one statement")
+
+ return statements[0]._parsed # pylint: disable=protected-access
+
+ @classmethod
+ def _extract_tables_from_statement(
+ cls,
+ parsed: exp.Expression,
+ engine: str,
+ ) -> set[Table]:
+ """
+ Find all referenced tables.
+ """
+ dialect = SQLGLOT_DIALECTS.get(engine)
+ return extract_tables_from_statement(parsed, dialect)
+
+ def is_mutating(self) -> bool:
+ """
+ Check if the statement mutates data (DDL/DML).
+
+ :return: True if the statement mutates data.
+ """
+ for node in self._parsed.walk():
+ if isinstance(
+ node,
+ (
+ exp.Insert,
+ exp.Update,
+ exp.Delete,
+ exp.Merge,
+ exp.Create,
+ exp.Drop,
+ exp.TruncateTable,
+ ),
+ ):
+ return True
+
+ if isinstance(node, exp.Command) and node.name == "ALTER":
+ return True
+
+ # Postgres runs DMLs prefixed by `EXPLAIN ANALYZE`, see
+ # https://www.postgresql.org/docs/current/sql-explain.html
+ if (
+ self._dialect == Dialects.POSTGRES
+ and isinstance(self._parsed, exp.Command)
+ and self._parsed.name == "EXPLAIN"
+ and self._parsed.expression.name.upper().startswith("ANALYZE ")
+ ):
+ analyzed_sql = self._parsed.expression.name[len("ANALYZE ") :]
+ return SQLStatement(analyzed_sql, self.engine).is_mutating()
+
+ return False
+
+ def format(self, comments: bool = True) -> str:
+ """
+ Pretty-format the SQL statement.
+ """
+ write = Dialect.get_or_raise(self._dialect)
+ return write.generate(self._parsed, copy=False, comments=comments, pretty=True)
+
+ def get_settings(self) -> dict[str, str | bool]:
+ """
+ Return the settings for the SQL statement.
+
+ >>> statement = SQLStatement("SET foo = 'bar'")
+ >>> statement.get_settings()
+ {"foo": "'bar'"}
+
+ """
+ return {
+ eq.this.sql(comments=False): eq.expression.sql(comments=False)
+ for set_item in self._parsed.find_all(exp.SetItem)
+ for eq in set_item.find_all(exp.EQ)
+ }
+
+
+class KQLSplitState(enum.Enum):
+ """
+ State machine for splitting a KQL script.
+
+ The state machine keeps track of whether we're inside a string or not, so we
+ don't split the script in a semi-colon that's part of a string.
+ """
+
+ OUTSIDE_STRING = enum.auto()
+ INSIDE_SINGLE_QUOTED_STRING = enum.auto()
+ INSIDE_DOUBLE_QUOTED_STRING = enum.auto()
+ INSIDE_MULTILINE_STRING = enum.auto()
+
+
+def split_kql(kql: str) -> list[str]:
+ """
+ Custom function for splitting KQL statements.
+ """
+ statements = []
+ state = KQLSplitState.OUTSIDE_STRING
+ statement_start = 0
+ script = kql if kql.endswith(";") else kql + ";"
+ for i, character in enumerate(script):
+ if state == KQLSplitState.OUTSIDE_STRING:
+ if character == ";":
+ statements.append(script[statement_start:i])
+ statement_start = i + 1
+ elif character == "'":
+ state = KQLSplitState.INSIDE_SINGLE_QUOTED_STRING
+ elif character == '"':
+ state = KQLSplitState.INSIDE_DOUBLE_QUOTED_STRING
+ elif character == "`" and script[i - 2 : i] == "``":
+ state = KQLSplitState.INSIDE_MULTILINE_STRING
+
+ elif (
+ state == KQLSplitState.INSIDE_SINGLE_QUOTED_STRING
+ and character == "'"
+ and script[i - 1] != "\\"
+ ):
+ state = KQLSplitState.OUTSIDE_STRING
+
+ elif (
+ state == KQLSplitState.INSIDE_DOUBLE_QUOTED_STRING
+ and character == '"'
+ and script[i - 1] != "\\"
+ ):
+ state = KQLSplitState.OUTSIDE_STRING
+
+ elif (
+ state == KQLSplitState.INSIDE_MULTILINE_STRING
+ and character == "`"
+ and script[i - 2 : i] == "``"
+ ):
+ state = KQLSplitState.OUTSIDE_STRING
+
+ return statements
+
+
+class KustoKQLStatement(BaseSQLStatement[str]):
+ """
+ Special class for Kusto KQL.
+
+ Kusto KQL is a SQL-like language, but it's not supported by sqlglot. Queries look
+ like this:
+
+ StormEvents
+ | summarize PropertyDamage = sum(DamageProperty) by State
+ | join kind=innerunique PopulationData on State
+ | project State, PropertyDamagePerCapita = PropertyDamage / Population
+ | sort by PropertyDamagePerCapita
+
+ See https://learn.microsoft.com/en-us/azure/data-explorer/kusto/query/ for more
+ details about it.
+ """
+
+ @classmethod
+ def split_script(
+ cls,
+ script: str,
+ engine: str,
+ ) -> list[KustoKQLStatement]:
+ """
+ Split a script at semi-colons.
+
+ Since we don't have a parser, we use a simple state machine based function. See
+ https://learn.microsoft.com/en-us/azure/data-explorer/kusto/query/scalar-data-types/string
+ for more information.
+ """
+ return [cls(statement, engine) for statement in split_kql(script)]
+
+ @classmethod
+ def _parse_statement(
+ cls,
+ statement: str,
+ engine: str,
+ ) -> str:
+ if engine != "kustokql":
+ raise SupersetParseError(f"Invalid engine: {engine}")
+
+ statements = split_kql(statement)
+ if len(statements) != 1:
+ raise SupersetParseError("SQLStatement should have exactly one statement")
+
+ return statements[0].strip()
+
+ @classmethod
+ def _extract_tables_from_statement(
+ cls,
+ parsed: str,
+ engine: str,
+ ) -> set[Table]:
+ """
+ Extract all tables referenced in the statement.
+
+ StormEvents
+ | where InjuriesDirect + InjuriesIndirect > 50
+ | join (PopulationData) on State
+ | project State, Population, TotalInjuries = InjuriesDirect + InjuriesIndirect
+
+ """
+ logger.warning(
+ "Kusto KQL doesn't support table extraction. This means that data access "
+ "roles will not be enforced by Superset in the database."
+ )
+ return set()
+
+ def format(self, comments: bool = True) -> str:
+ """
+ Pretty-format the SQL statement.
+ """
+ return self._parsed
+
+ def get_settings(self) -> dict[str, str | bool]:
+ """
+ Return the settings for the SQL statement.
+
+ >>> statement = KustoKQLStatement("set querytrace;")
+ >>> statement.get_settings()
+ {"querytrace": True}
+
+ """
+ set_regex = r"^set\s+(?P\w+)(?:\s*=\s*(?P\w+))?$"
+ if match := re.match(set_regex, self._parsed, re.IGNORECASE):
+ return {match.group("name"): match.group("value") or True}
+
+ return {}
+
+ def is_mutating(self) -> bool:
+ """
+ Check if the statement mutates data (DDL/DML).
+
+ :return: True if the statement mutates data.
+ """
+ return self._parsed.startswith(".") and not self._parsed.startswith(".show")
+
+
+class SQLScript:
+ """
+ A SQL script, with 0+ statements.
+ """
+
+ # Special engines that can't be parsed using sqlglot. Supporting non-SQL engines
+ # adds a lot of complexity to Superset, so we should avoid adding new engines to
+ # this data structure.
+ special_engines = {
+ "kustokql": KustoKQLStatement,
+ }
+
+ def __init__(
+ self,
+ script: str,
+ engine: str,
+ ):
+ statement_class = self.special_engines.get(engine, SQLStatement)
+ self.engine = engine
+ self.statements = statement_class.split_script(script, engine)
+
+ def format(self, comments: bool = True) -> str:
+ """
+ Pretty-format the SQL script.
+ """
+ return ";\n".join(statement.format(comments) for statement in self.statements)
+
+ def get_settings(self) -> dict[str, str | bool]:
+ """
+ Return the settings for the SQL script.
+
+ >>> statement = SQLScript("SET foo = 'bar'; SET foo = 'baz'")
+ >>> statement.get_settings()
+ {"foo": "'baz'"}
+
+ """
+ settings: dict[str, str | bool] = {}
+ for statement in self.statements:
+ settings.update(statement.get_settings())
+
+ return settings
+
+ def has_mutation(self) -> bool:
+ """
+ Check if the script contains mutating statements.
+
+ :return: True if the script contains mutating statements
+ """
+ return any(statement.is_mutating() for statement in self.statements)
+
+
+def extract_tables_from_statement(
+ statement: exp.Expression,
+ dialect: Dialects | None,
+) -> set[Table]:
+ """
+ Extract all table references in a single statement.
+
+ Please not that this is not trivial; consider the following queries:
+
+ DESCRIBE some_table;
+ SHOW PARTITIONS FROM some_table;
+ WITH masked_name AS (SELECT * FROM some_table) SELECT * FROM masked_name;
+
+ See the unit tests for other tricky cases.
+ """
+ sources: Iterable[exp.Table]
+
+ if isinstance(statement, exp.Describe):
+ # A `DESCRIBE` query has no sources in sqlglot, so we need to explicitly
+ # query for all tables.
+ sources = statement.find_all(exp.Table)
+ elif isinstance(statement, exp.Command):
+ # Commands, like `SHOW COLUMNS FROM foo`, have to be converted into a
+ # `SELECT` statetement in order to extract tables.
+ literal = statement.find(exp.Literal)
+ if not literal:
+ return set()
+
+ try:
+ pseudo_query = sqlglot.parse_one(f"SELECT {literal.this}", dialect=dialect)
+ except ParseError:
+ return set()
+ sources = pseudo_query.find_all(exp.Table)
+ else:
+ sources = [
+ source
+ for scope in traverse_scope(statement)
+ for source in scope.sources.values()
+ if isinstance(source, exp.Table) and not is_cte(source, scope)
+ ]
+
+ return {
+ Table(
+ source.name,
+ source.db if source.db != "" else None,
+ source.catalog if source.catalog != "" else None,
+ )
+ for source in sources
+ }
+
+
+def is_cte(source: exp.Table, scope: Scope) -> bool:
+ """
+ Is the source a CTE?
+
+ CTEs in the parent scope look like tables (and are represented by
+ exp.Table objects), but should not be considered as such;
+ otherwise a user with access to table `foo` could access any table
+ with a query like this:
+
+ WITH foo AS (SELECT * FROM target_table) SELECT * FROM foo
+
+ """
+ parent_sources = scope.parent.sources if scope.parent else {}
+ ctes_in_scope = {
+ name
+ for name, parent_scope in parent_sources.items()
+ if isinstance(parent_scope, Scope) and parent_scope.scope_type == ScopeType.CTE
+ }
+
+ return source.name in ctes_in_scope
diff --git a/superset/sql_lab.py b/superset/sql_lab.py
index f20bff35c30c9..65a093610d114 100644
--- a/superset/sql_lab.py
+++ b/superset/sql_lab.py
@@ -46,17 +46,18 @@
OAuth2RedirectError,
SupersetErrorException,
SupersetErrorsException,
+ SupersetParseError,
)
from superset.extensions import celery_app, event_logger
from superset.models.core import Database
from superset.models.sql_lab import Query
from superset.result_set import SupersetResultSet
+from superset.sql.parse import SQLStatement, Table
from superset.sql_parse import (
CtasMethod,
insert_rls_as_subquery,
insert_rls_in_predicate,
ParsedQuery,
- Table,
)
from superset.sqllab.limiting_factor import LimitingFactor
from superset.sqllab.utils import write_ipc_buffer
@@ -194,7 +195,7 @@ def get_sql_results( # pylint: disable=too-many-arguments
return handle_query_error(ex, query)
-def execute_sql_statement( # pylint: disable=too-many-statements
+def execute_sql_statement( # pylint: disable=too-many-statements, too-many-locals
sql_statement: str,
query: Query,
cursor: Any,
@@ -236,14 +237,27 @@ def execute_sql_statement( # pylint: disable=too-many-statements
# We are testing to see if more rows exist than the limit.
increased_limit = None if query.limit is None else query.limit + 1
- if not db_engine_spec.is_readonly_query(parsed_query) and not database.allow_dml:
- raise SupersetErrorException(
- SupersetError(
- message=__("Only SELECT statements are allowed against this database."),
- error_type=SupersetErrorType.DML_NOT_ALLOWED_ERROR,
- level=ErrorLevel.ERROR,
+ if not database.allow_dml:
+ try:
+ parsed_statement = SQLStatement(sql_statement, engine=db_engine_spec.engine)
+ disallowed = parsed_statement.is_mutating()
+ except SupersetParseError:
+ # if we fail to parse teh query, disallow by default
+ disallowed = True
+
+ if disallowed:
+ raise SupersetErrorException(
+ SupersetError(
+ message=__(
+ "This database does not allow for DDL/DML, and the query "
+ "could not be parsed to confirm it is a read-only query. Please "
+ "contact your administrator for more assistance."
+ ),
+ error_type=SupersetErrorType.DML_NOT_ALLOWED_ERROR,
+ level=ErrorLevel.ERROR,
+ )
)
- )
+
if apply_ctas:
if not query.tmp_table_name:
start_dttm = datetime.fromtimestamp(query.start_time)
diff --git a/superset/sql_parse.py b/superset/sql_parse.py
index cf78431753935..91b68126356a2 100644
--- a/superset/sql_parse.py
+++ b/superset/sql_parse.py
@@ -19,23 +19,16 @@
from __future__ import annotations
-import enum
import logging
import re
-import urllib.parse
-from collections.abc import Iterable, Iterator
-from dataclasses import dataclass
-from typing import Any, cast, Generic, TYPE_CHECKING, TypeVar
+from collections.abc import Iterator
+from typing import Any, cast, TYPE_CHECKING
-import sqlglot
import sqlparse
from flask_babel import gettext as __
from jinja2 import nodes
from sqlalchemy import and_
-from sqlglot import exp, parse, parse_one
-from sqlglot.dialects.dialect import Dialect, Dialects
-from sqlglot.errors import ParseError, SqlglotError
-from sqlglot.optimizer.scope import Scope, ScopeType, traverse_scope
+from sqlglot.dialects.dialect import Dialects
from sqlparse import keywords
from sqlparse.lexer import Lexer
from sqlparse.sql import (
@@ -68,6 +61,7 @@
SupersetParseError,
SupersetSecurityException,
)
+from superset.sql.parse import extract_tables_from_statement, SQLScript, Table
from superset.utils.backports import StrEnum
try:
@@ -226,7 +220,9 @@ def get_cte_remainder_query(sql: str) -> tuple[str | None, str]:
def check_sql_functions_exist(
- sql: str, function_list: set[str], engine: str | None = None
+ sql: str,
+ function_list: set[str],
+ engine: str = "base",
) -> bool:
"""
Check if the SQL statement contains any of the specified functions.
@@ -238,7 +234,7 @@ def check_sql_functions_exist(
return ParsedQuery(sql, engine=engine).check_functions_exist(function_list)
-def strip_comments_from_sql(statement: str, engine: str | None = None) -> str:
+def strip_comments_from_sql(statement: str, engine: str = "base") -> str:
"""
Strips comments from a SQL statement, does a simple test first
to avoid always instantiating the expensive ParsedQuery constructor
@@ -255,493 +251,18 @@ def strip_comments_from_sql(statement: str, engine: str | None = None) -> str:
)
-@dataclass(eq=True, frozen=True)
-class Table:
- """
- A fully qualified SQL table conforming to [[catalog.]schema.]table.
- """
-
- table: str
- schema: str | None = None
- catalog: str | None = None
-
- def __str__(self) -> str:
- """
- Return the fully qualified SQL table name.
- """
-
- return ".".join(
- urllib.parse.quote(part, safe="").replace(".", "%2E")
- for part in [self.catalog, self.schema, self.table]
- if part
- )
-
- def __eq__(self, __o: object) -> bool:
- return str(self) == str(__o)
-
-
-def extract_tables_from_statement(
- statement: exp.Expression,
- dialect: Dialects | None,
-) -> set[Table]:
- """
- Extract all table references in a single statement.
-
- Please not that this is not trivial; consider the following queries:
-
- DESCRIBE some_table;
- SHOW PARTITIONS FROM some_table;
- WITH masked_name AS (SELECT * FROM some_table) SELECT * FROM masked_name;
-
- See the unit tests for other tricky cases.
- """
- sources: Iterable[exp.Table]
-
- if isinstance(statement, exp.Describe):
- # A `DESCRIBE` query has no sources in sqlglot, so we need to explicitly
- # query for all tables.
- sources = statement.find_all(exp.Table)
- elif isinstance(statement, exp.Command):
- # Commands, like `SHOW COLUMNS FROM foo`, have to be converted into a
- # `SELECT` statetement in order to extract tables.
- literal = statement.find(exp.Literal)
- if not literal:
- return set()
-
- try:
- pseudo_query = parse_one(f"SELECT {literal.this}", dialect=dialect)
- except ParseError:
- return set()
- sources = pseudo_query.find_all(exp.Table)
- else:
- sources = [
- source
- for scope in traverse_scope(statement)
- for source in scope.sources.values()
- if isinstance(source, exp.Table) and not is_cte(source, scope)
- ]
-
- return {
- Table(
- source.name,
- source.db if source.db != "" else None,
- source.catalog if source.catalog != "" else None,
- )
- for source in sources
- }
-
-
-def is_cte(source: exp.Table, scope: Scope) -> bool:
- """
- Is the source a CTE?
-
- CTEs in the parent scope look like tables (and are represented by
- exp.Table objects), but should not be considered as such;
- otherwise a user with access to table `foo` could access any table
- with a query like this:
-
- WITH foo AS (SELECT * FROM target_table) SELECT * FROM foo
-
- """
- parent_sources = scope.parent.sources if scope.parent else {}
- ctes_in_scope = {
- name
- for name, parent_scope in parent_sources.items()
- if isinstance(parent_scope, Scope) and parent_scope.scope_type == ScopeType.CTE
- }
-
- return source.name in ctes_in_scope
-
-
-# To avoid unnecessary parsing/formatting of queries, the statement has the concept of
-# an "internal representation", which is the AST of the SQL statement. For most of the
-# engines supported by Superset this is `sqlglot.exp.Expression`, but there is a special
-# case: KustoKQL uses a different syntax and there are no Python parsers for it, so we
-# store the AST as a string (the original query), and manipulate it with regular
-# expressions.
-InternalRepresentation = TypeVar("InternalRepresentation")
-
-# The base type. This helps type checking the `split_query` method correctly, since each
-# derived class has a more specific return type (the class itself). This will no longer
-# be needed once Python 3.11 is the lowest version supported. See PEP 673 for more
-# information: https://peps.python.org/pep-0673/
-TBaseSQLStatement = TypeVar("TBaseSQLStatement") # pylint: disable=invalid-name
-
-
-class BaseSQLStatement(Generic[InternalRepresentation]):
- """
- Base class for SQL statements.
-
- The class can be instantiated with a string representation of the query or, for
- efficiency reasons, with a pre-parsed AST. This is useful with `sqlglot.parse`,
- which will split a query in multiple already parsed statements.
-
- The `engine` parameters comes from the `engine` attribute in a Superset DB engine
- spec.
- """
-
- def __init__(
- self,
- statement: str | InternalRepresentation,
- engine: str,
- ):
- self._parsed: InternalRepresentation = (
- self._parse_statement(statement, engine)
- if isinstance(statement, str)
- else statement
- )
- self.engine = engine
- self.tables = self._extract_tables_from_statement(self._parsed, self.engine)
-
- @classmethod
- def split_query(
- cls: type[TBaseSQLStatement],
- query: str,
- engine: str,
- ) -> list[TBaseSQLStatement]:
- """
- Split a query into multiple instantiated statements.
-
- This is a helper function to split a full SQL query into multiple
- `BaseSQLStatement` instances. It's used by `SQLScript` when instantiating the
- statements within a query.
- """
- raise NotImplementedError()
-
- @classmethod
- def _parse_statement(
- cls,
- statement: str,
- engine: str,
- ) -> InternalRepresentation:
- """
- Parse a string containing a single SQL statement, and returns the parsed AST.
-
- Derived classes should not assume that `statement` contains a single statement,
- and MUST explicitly validate that. Since this validation is parser dependent the
- responsibility is left to the children classes.
- """
- raise NotImplementedError()
-
- @classmethod
- def _extract_tables_from_statement(
- cls,
- parsed: InternalRepresentation,
- engine: str,
- ) -> set[Table]:
- """
- Extract all table references in a given statement.
- """
- raise NotImplementedError()
-
- def format(self, comments: bool = True) -> str:
- """
- Format the statement, optionally ommitting comments.
- """
- raise NotImplementedError()
-
- def get_settings(self) -> dict[str, str | bool]:
- """
- Return any settings set by the statement.
-
- For example, for this statement:
-
- sql> SET foo = 'bar';
-
- The method should return `{"foo": "'bar'"}`. Note the single quotes.
- """
- raise NotImplementedError()
-
- def __str__(self) -> str:
- return self.format()
-
-
-class SQLStatement(BaseSQLStatement[exp.Expression]):
- """
- A SQL statement.
-
- This class is used for all engines with dialects that can be parsed using sqlglot.
- """
-
- def __init__(
- self,
- statement: str | exp.Expression,
- engine: str,
- ):
- self._dialect = SQLGLOT_DIALECTS.get(engine)
- super().__init__(statement, engine)
-
- @classmethod
- def split_query(
- cls,
- query: str,
- engine: str,
- ) -> list[SQLStatement]:
- dialect = SQLGLOT_DIALECTS.get(engine)
-
- try:
- statements = sqlglot.parse(query, dialect=dialect)
- except sqlglot.errors.ParseError as ex:
- raise SupersetParseError("Unable to split query") from ex
-
- return [cls(statement, engine) for statement in statements if statement]
-
- @classmethod
- def _parse_statement(
- cls,
- statement: str,
- engine: str,
- ) -> exp.Expression:
- """
- Parse a single SQL statement.
- """
- dialect = SQLGLOT_DIALECTS.get(engine)
-
- # We could parse with `sqlglot.parse_one` to get a single statement, but we need
- # to verify that the string contains exactly one statement.
- try:
- statements = sqlglot.parse(statement, dialect=dialect)
- except sqlglot.errors.ParseError as ex:
- raise SupersetParseError("Unable to split query") from ex
-
- statements = [statement for statement in statements if statement]
- if len(statements) != 1:
- raise SupersetParseError("SQLStatement should have exactly one statement")
-
- return statements[0]
-
- @classmethod
- def _extract_tables_from_statement(
- cls,
- parsed: exp.Expression,
- engine: str,
- ) -> set[Table]:
- """
- Find all referenced tables.
- """
- dialect = SQLGLOT_DIALECTS.get(engine)
- return extract_tables_from_statement(parsed, dialect)
-
- def format(self, comments: bool = True) -> str:
- """
- Pretty-format the SQL statement.
- """
- write = Dialect.get_or_raise(self._dialect)
- return write.generate(self._parsed, copy=False, comments=comments, pretty=True)
-
- def get_settings(self) -> dict[str, str | bool]:
- """
- Return the settings for the SQL statement.
-
- >>> statement = SQLStatement("SET foo = 'bar'")
- >>> statement.get_settings()
- {"foo": "'bar'"}
-
- """
- return {
- eq.this.sql(): eq.expression.sql()
- for set_item in self._parsed.find_all(exp.SetItem)
- for eq in set_item.find_all(exp.EQ)
- }
-
-
-class KQLSplitState(enum.Enum):
- """
- State machine for splitting a KQL query.
-
- The state machine keeps track of whether we're inside a string or not, so we
- don't split the query in a semi-colon that's part of a string.
- """
-
- OUTSIDE_STRING = enum.auto()
- INSIDE_SINGLE_QUOTED_STRING = enum.auto()
- INSIDE_DOUBLE_QUOTED_STRING = enum.auto()
- INSIDE_MULTILINE_STRING = enum.auto()
-
-
-def split_kql(kql: str) -> list[str]:
- """
- Custom function for splitting KQL statements.
- """
- statements = []
- state = KQLSplitState.OUTSIDE_STRING
- statement_start = 0
- query = kql if kql.endswith(";") else kql + ";"
- for i, character in enumerate(query):
- if state == KQLSplitState.OUTSIDE_STRING:
- if character == ";":
- statements.append(query[statement_start:i])
- statement_start = i + 1
- elif character == "'":
- state = KQLSplitState.INSIDE_SINGLE_QUOTED_STRING
- elif character == '"':
- state = KQLSplitState.INSIDE_DOUBLE_QUOTED_STRING
- elif character == "`" and query[i - 2 : i] == "``":
- state = KQLSplitState.INSIDE_MULTILINE_STRING
-
- elif (
- state == KQLSplitState.INSIDE_SINGLE_QUOTED_STRING
- and character == "'"
- and query[i - 1] != "\\"
- ):
- state = KQLSplitState.OUTSIDE_STRING
-
- elif (
- state == KQLSplitState.INSIDE_DOUBLE_QUOTED_STRING
- and character == '"'
- and query[i - 1] != "\\"
- ):
- state = KQLSplitState.OUTSIDE_STRING
-
- elif (
- state == KQLSplitState.INSIDE_MULTILINE_STRING
- and character == "`"
- and query[i - 2 : i] == "``"
- ):
- state = KQLSplitState.OUTSIDE_STRING
-
- return statements
-
-
-class KustoKQLStatement(BaseSQLStatement[str]):
- """
- Special class for Kusto KQL.
-
- Kusto KQL is a SQL-like language, but it's not supported by sqlglot. Queries look
- like this:
-
- StormEvents
- | summarize PropertyDamage = sum(DamageProperty) by State
- | join kind=innerunique PopulationData on State
- | project State, PropertyDamagePerCapita = PropertyDamage / Population
- | sort by PropertyDamagePerCapita
-
- See https://learn.microsoft.com/en-us/azure/data-explorer/kusto/query/ for more
- details about it.
- """
-
- @classmethod
- def split_query(
- cls,
- query: str,
- engine: str,
- ) -> list[KustoKQLStatement]:
- """
- Split a query at semi-colons.
-
- Since we don't have a parser, we use a simple state machine based function. See
- https://learn.microsoft.com/en-us/azure/data-explorer/kusto/query/scalar-data-types/string
- for more information.
- """
- return [cls(statement, engine) for statement in split_kql(query)]
-
- @classmethod
- def _parse_statement(
- cls,
- statement: str,
- engine: str,
- ) -> str:
- if engine != "kustokql":
- raise SupersetParseError(f"Invalid engine: {engine}")
-
- statements = split_kql(statement)
- if len(statements) != 1:
- raise SupersetParseError("SQLStatement should have exactly one statement")
-
- return statements[0].strip()
-
- @classmethod
- def _extract_tables_from_statement(cls, parsed: str, engine: str) -> set[Table]:
- """
- Extract all tables referenced in the statement.
-
- StormEvents
- | where InjuriesDirect + InjuriesIndirect > 50
- | join (PopulationData) on State
- | project State, Population, TotalInjuries = InjuriesDirect + InjuriesIndirect
-
- """
- logger.warning(
- "Kusto KQL doesn't support table extraction. This means that data access "
- "roles will not be enforced by Superset in the database."
- )
- return set()
-
- def format(self, comments: bool = True) -> str:
- """
- Pretty-format the SQL statement.
- """
- return self._parsed
-
- def get_settings(self) -> dict[str, str | bool]:
- """
- Return the settings for the SQL statement.
-
- >>> statement = KustoKQLStatement("set querytrace;")
- >>> statement.get_settings()
- {"querytrace": True}
-
- """
- set_regex = r"^set\s+(?P\w+)(?:\s*=\s*(?P\w+))?$"
- if match := re.match(set_regex, self._parsed, re.IGNORECASE):
- return {match.group("name"): match.group("value") or True}
-
- return {}
-
-
-class SQLScript:
- """
- A SQL script, with 0+ statements.
- """
-
- # Special engines that can't be parsed using sqlglot. Supporting non-SQL engines
- # adds a lot of complexity to Superset, so we should avoid adding new engines to
- # this data structure.
- special_engines = {
- "kustokql": KustoKQLStatement,
- }
-
- def __init__(
- self,
- query: str,
- engine: str,
- ):
- statement_class = self.special_engines.get(engine, SQLStatement)
- self.statements = statement_class.split_query(query, engine)
-
- def format(self, comments: bool = True) -> str:
- """
- Pretty-format the SQL query.
- """
- return ";\n".join(statement.format(comments) for statement in self.statements)
-
- def get_settings(self) -> dict[str, str | bool]:
- """
- Return the settings for the SQL query.
-
- >>> statement = SQLScript("SET foo = 'bar'; SET foo = 'baz'")
- >>> statement.get_settings()
- {"foo": "'baz'"}
-
- """
- settings: dict[str, str | bool] = {}
- for statement in self.statements:
- settings.update(statement.get_settings())
-
- return settings
-
-
class ParsedQuery:
def __init__(
self,
sql_statement: str,
strip_comments: bool = False,
- engine: str | None = None,
+ engine: str = "base",
):
if strip_comments:
sql_statement = sqlparse.format(sql_statement, strip_comments=True)
self.sql: str = sql_statement
+ self._engine = engine
self._dialect = SQLGLOT_DIALECTS.get(engine) if engine else None
self._tables: set[Table] = set()
self._alias_names: set[str] = set()
@@ -793,24 +314,18 @@ def _extract_tables_from_sql(self) -> set[Table]:
Note: this uses sqlglot, since it's better at catching more edge cases.
"""
try:
- statements = parse(self.stripped(), dialect=self._dialect)
- except SqlglotError as ex:
+ statements = [
+ statement._parsed # pylint: disable=protected-access
+ for statement in SQLScript(self.stripped(), self._engine).statements
+ ]
+ except SupersetParseError as ex:
logger.warning("Unable to parse SQL (%s): %s", self._dialect, self.sql)
-
- message = (
- "Error parsing near '{highlight}' at line {line}:{col}".format( # pylint: disable=consider-using-f-string
- **ex.errors[0]
- )
- if isinstance(ex, ParseError)
- else str(ex)
- )
-
raise SupersetSecurityException(
SupersetError(
error_type=SupersetErrorType.QUERY_SECURITY_ACCESS_ERROR,
message=__(
"You may have an error in your SQL statement. {message}"
- ).format(message=message),
+ ).format(message=ex.error.message),
level=ErrorLevel.ERROR,
)
) from ex
@@ -822,77 +337,6 @@ def _extract_tables_from_sql(self) -> set[Table]:
if statement
}
- def _extract_tables_from_statement(self, statement: exp.Expression) -> set[Table]:
- """
- Extract all table references in a single statement.
-
- Please not that this is not trivial; consider the following queries:
-
- DESCRIBE some_table;
- SHOW PARTITIONS FROM some_table;
- WITH masked_name AS (SELECT * FROM some_table) SELECT * FROM masked_name;
-
- See the unit tests for other tricky cases.
- """
- sources: Iterable[exp.Table]
-
- if isinstance(statement, exp.Describe):
- # A `DESCRIBE` query has no sources in sqlglot, so we need to explicitly
- # query for all tables.
- sources = statement.find_all(exp.Table)
- elif isinstance(statement, exp.Command):
- # Commands, like `SHOW COLUMNS FROM foo`, have to be converted into a
- # `SELECT` statetement in order to extract tables.
- if not (literal := statement.find(exp.Literal)):
- return set()
-
- try:
- pseudo_query = parse_one(
- f"SELECT {literal.this}",
- dialect=self._dialect,
- )
- sources = pseudo_query.find_all(exp.Table)
- except SqlglotError:
- return set()
- else:
- sources = [
- source
- for scope in traverse_scope(statement)
- for source in scope.sources.values()
- if isinstance(source, exp.Table) and not self._is_cte(source, scope)
- ]
-
- return {
- Table(
- source.name,
- source.db if source.db != "" else None,
- source.catalog if source.catalog != "" else None,
- )
- for source in sources
- }
-
- def _is_cte(self, source: exp.Table, scope: Scope) -> bool:
- """
- Is the source a CTE?
-
- CTEs in the parent scope look like tables (and are represented by
- exp.Table objects), but should not be considered as such;
- otherwise a user with access to table `foo` could access any table
- with a query like this:
-
- WITH foo AS (SELECT * FROM target_table) SELECT * FROM foo
-
- """
- parent_sources = scope.parent.sources if scope.parent else {}
- ctes_in_scope = {
- name
- for name, parent_scope in parent_sources.items()
- if isinstance(parent_scope, Scope)
- and parent_scope.scope_type == ScopeType.CTE
- }
-
- return source.name in ctes_in_scope
-
@property
def limit(self) -> int | None:
return self._limit
diff --git a/superset/sqllab/api.py b/superset/sqllab/api.py
index f7d66ed4e19fa..2403a36583e7b 100644
--- a/superset/sqllab/api.py
+++ b/superset/sqllab/api.py
@@ -35,8 +35,8 @@
from superset.extensions import event_logger
from superset.jinja_context import get_template_processor
from superset.models.sql_lab import Query
+from superset.sql.parse import SQLScript
from superset.sql_lab import get_sql_results
-from superset.sql_parse import SQLScript
from superset.sqllab.command_status import SqlJsonExecutionStatus
from superset.sqllab.exceptions import (
QueryIsForbiddenToAccessException,
diff --git a/superset/sqllab/sql_json_executer.py b/superset/sqllab/sql_json_executer.py
index ac9968ed6b467..27483fb31cb01 100644
--- a/superset/sqllab/sql_json_executer.py
+++ b/superset/sqllab/sql_json_executer.py
@@ -90,7 +90,6 @@ def execute(
rendered_query: str,
log_params: dict[str, Any] | None,
) -> SqlJsonExecutionStatus:
- print(">>> execute <<<")
query_id = execution_context.query.id
try:
data = self._get_sql_results_with_timeout(
@@ -102,7 +101,6 @@ def execute(
raise
except Exception as ex:
logger.exception("Query %i failed unexpectedly", query_id)
- print(str(ex))
raise SupersetGenericDBErrorException(
utils.error_msg_from_exception(ex)
) from ex
diff --git a/superset/tasks/thumbnails.py b/superset/tasks/thumbnails.py
index 483fb8495456c..dd9b5065dce34 100644
--- a/superset/tasks/thumbnails.py
+++ b/superset/tasks/thumbnails.py
@@ -24,6 +24,7 @@
from superset import security_manager, thumbnail_cache
from superset.extensions import celery_app
+from superset.security.guest_token import GuestToken
from superset.tasks.utils import get_executor
from superset.utils.core import override_user
from superset.utils.screenshots import ChartScreenshot, DashboardScreenshot
@@ -85,6 +86,7 @@ def cache_dashboard_thumbnail(
if not thumbnail_cache:
logging.warning("No cache set, refusing to compute")
return
+
dashboard = Dashboard.get(dashboard_id)
url = get_url_path("Superset.dashboard", dashboard_id_or_slug=dashboard.id)
@@ -106,13 +108,14 @@ def cache_dashboard_thumbnail(
)
-# pylint: disable=too-many-arguments
@celery_app.task(name="cache_dashboard_screenshot", soft_time_limit=300)
-def cache_dashboard_screenshot(
- current_user: Optional[str],
+def cache_dashboard_screenshot( # pylint: disable=too-many-arguments
+ username: str,
dashboard_id: int,
dashboard_url: str,
force: bool = True,
+ cache_key: Optional[str] = None,
+ guest_token: Optional[GuestToken] = None,
thumb_size: Optional[WindowSize] = None,
window_size: Optional[WindowSize] = None,
) -> None:
@@ -126,18 +129,25 @@ def cache_dashboard_screenshot(
dashboard = Dashboard.get(dashboard_id)
logger.info("Caching dashboard: %s", dashboard_url)
- _, username = get_executor(
- executor_types=current_app.config["THUMBNAIL_EXECUTE_AS"],
- model=dashboard,
- current_user=current_user,
- )
- user = security_manager.find_user(username)
- with override_user(user):
+
+ # Requests from Embedded should always use the Guest user
+ if guest_token:
+ current_user = security_manager.get_guest_user_from_token(guest_token)
+ else:
+ _, exec_username = get_executor(
+ executor_types=current_app.config["THUMBNAIL_EXECUTE_AS"],
+ model=dashboard,
+ current_user=username,
+ )
+ current_user = security_manager.find_user(exec_username)
+
+ with override_user(current_user):
screenshot = DashboardScreenshot(dashboard_url, dashboard.digest)
screenshot.compute_and_cache(
- user=user,
+ user=current_user,
cache=thumbnail_cache,
force=force,
window_size=window_size,
thumb_size=thumb_size,
+ cache_key=cache_key,
)
diff --git a/superset/templates/appbuilder/navbar.html b/superset/templates/appbuilder/navbar.html
index 3db7f5de6583e..dda9c2430af08 100644
--- a/superset/templates/appbuilder/navbar.html
+++ b/superset/templates/appbuilder/navbar.html
@@ -36,6 +36,7 @@
width="{{ app_icon_width }}"
src="{{ appbuilder.app_icon }}"
alt="{{ appbuilder.app_name }}"
+ loading="lazy"
/>
diff --git a/superset/templates/superset/basic.html b/superset/templates/superset/basic.html
index b97ecb338cfa4..49cd8aa10e6cb 100644
--- a/superset/templates/superset/basic.html
+++ b/superset/templates/superset/basic.html
@@ -1,124 +1,137 @@
-{# 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. #}
-
-{% import 'appbuilder/general/lib.html' as lib %} {% from
-'superset/partials/asset_bundle.html' import css_bundle, js_bundle with context
-%} {% set favicons = appbuilder.app.config['FAVICONS'] %}
-
-
- {% include "head_custom_extra.html" %}
-
- {% block title %} {% if title %} {{ title }} {% elif appbuilder and
- appbuilder.app_name %} {{ appbuilder.app_name }} {% endif %} {% endblock
- %}
-
- {% block head_meta %}{% endblock %} {% block head_css %} {% for favicon in
- favicons %} {%
- endfor %}
-
-
-
-
-
-
+{# 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. #}
- {{ css_bundle("theme") }} {% if entry %} {{ css_bundle(entry) }} {% endif %}
- {% endblock %} {{ js_bundle("theme") }}
+
+ {% import 'appbuilder/general/lib.html' as lib %} {% from
+ 'superset/partials/asset_bundle.html' import css_bundle, js_bundle with context
+ %} {% set favicons = appbuilder.app.config['FAVICONS'] %}
+ {% import "superset/macros.html" as macros %}
+
+
+ {% include "head_custom_extra.html" %}
+
+ {% block title %} {% if title %} {{ title }} {% elif appbuilder and
+ appbuilder.app_name %} {{ appbuilder.app_name }} {% endif %} {% endblock
+ %}
+
+ {% block head_meta %}{% endblock %} {% block head_css %} {% for favicon in
+ favicons %} {%
+ endfor %}
+
+
+
+
+
-
-
+ {{ css_bundle("theme") }} {% if entry %} {{ css_bundle(entry) }} {% endif %}
+ {% endblock %} {{ js_bundle("theme") }}
-
- {% block navbar %} {% if not standalone_mode %} {% include
- 'appbuilder/navbar.html' %} {% endif %} {% endblock %} {% block body %}
-
-
-
- {% endblock %}
+
-
-
-
-
-
-
-
-
-
-
- {% block tail_js %} {% if not standalone_mode %} {{ js_bundle('menu') }} {%
- endif %} {% if entry %} {{ js_bundle(entry) }} {% endif %} {% include
- "tail_js_custom_extra.html" %} {% endblock %}
-
-
+ {% block tail_js %} {% if not standalone_mode %} {{ js_bundle('menu') }} {%
+ endif %} {% if entry %} {{ js_bundle(entry) }} {% endif %} {% include
+ "tail_js_custom_extra.html" %} {% endblock %}
+
+
+
+
+
diff --git a/superset/templates/superset/theme.html b/superset/templates/superset/theme.html
deleted file mode 100644
index 3f6c8fb0745fb..0000000000000
--- a/superset/templates/superset/theme.html
+++ /dev/null
@@ -1,1355 +0,0 @@
-{#
- 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.
-#}
-{% extends "superset/basic.html" %}
-{% import "superset/macros.html" as macros %}
-
-{% block body %}
-
-
Vivamus sagittis lacus vel augue laoreet rutrum faucibus dolor auctor.
-
-
-
-
-
Example body text
-
Nullam quis risus eget urna mollis ornare vel eu leo. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nullam id dolor id nibh ultricies vehicula.
-
This line of text is meant to be treated as fine print.
-
The following snippet of text is rendered as bold text.
-
The following snippet of text is rendered as italicized text.
-
An abbreviation of the word attribute is attr.
-
-
-
-
-
-
Emphasis classes
-
Fusce dapibus, tellus ac cursus commodo, tortor mauris nibh.
-
Nullam id dolor id nibh ultricies vehicula ut id elit.
-
Etiam porta sem malesuada magna mollis euismod.
-
Donec ullamcorper nulla non metus auctor fringilla.
-
Duis mollis, est non commodo luctus, nisi erat porttitor ligula.
-
Maecenas sed diam eget risus varius blandit sit amet non magna.
-
-
-
-
-
-
-
-
-
-
Blockquotes
-
-
-
-
-
-
-
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer posuere erat a ante.
- Someone famous in Source Title
-
-
-
-
-
-
-
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer posuere erat a ante.
Raw denim you probably haven't heard of them jean shorts Austin. Nesciunt tofu stumptown aliqua, retro synth master cleanse. Mustache cliche tempor, williamsburg carles vegan helvetica. Reprehenderit butcher retro keffiyeh dreamcatcher synth. Cosby sweater eu banh mi, qui irure terry richardson ex squid. Aliquip placeat salvia cillum iphone. Seitan aliquip quis cardigan american apparel, butcher voluptate nisi qui.
-
-
-
Food truck fixie locavore, accusamus mcsweeney's marfa nulla single-origin coffee squid. Exercitation +1 labore velit, blog sartorial PBR leggings next level wes anderson artisan four loko farm-to-table craft beer twee. Qui photo booth letterpress, commodo enim craft beer mlkshk aliquip jean shorts ullamco ad vinyl cillum PBR. Homo nostrud organic, assumenda labore aesthetic magna delectus mollit.
-
-
-
Etsy mixtape wayfarers, ethical wes anderson tofu before they sold out mcsweeney's organic lomo retro fanny pack lo-fi farm-to-table readymade. Messenger bag gentrify pitchfork tattooed craft beer, iphone skateboard locavore carles etsy salvia banksy hoodie helvetica. DIY synth PBR banksy irony. Leggings gentrify squid 8-bit cred pitchfork.
-
-
-
Trust fund seitan letterpress, keytar raw denim keffiyeh etsy art party before they sold out master cleanse gluten-free squid scenester freegan cosby sweater. Fanny pack portland seitan DIY, art party locavore wolf cliche high life echo park Austin. Cred vinyl keffiyeh DIY salvia PBR, banh mi before they sold out farm-to-table VHS viral locavore cosby sweater.
Best check yo self, you're not looking too good. Nulla vitae elit libero, a pharetra augue. Praesent commodo cursus magna, vel scelerisque nisl consectetur et.