refactor(explore): improve typing for Dnd controls (#16362)

This commit is contained in:
Jesse Yang 2021-08-26 01:23:14 -07:00 committed by GitHub
parent 18be181946
commit ec087507e5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 237 additions and 168 deletions

View File

@ -57,12 +57,15 @@ export type DashboardLayoutState = { present: DashboardLayout };
export type DashboardState = {
preselectNativeFilters?: JsonObject;
editMode: boolean;
isPublished: boolean;
directPathToChild: string[];
activeTabs: ActiveTabs;
fullSizeChartId: number | null;
isRefreshing: boolean;
hasUnsavedChanges: boolean;
};
export type DashboardInfo = {
id: number;
common: {
flash_messages: string[];
conf: JsonObject;

View File

@ -18,6 +18,7 @@
*/
import React, { ReactNode, useCallback, useState } from 'react';
import { ControlType } from '@superset-ui/chart-controls';
import { ControlComponentProps as BaseControlComponentProps } from '@superset-ui/chart-controls/lib/shared-controls/components/types';
import { JsonValue, QueryFormData } from '@superset-ui/core';
import ErrorBoundary from 'src/components/ErrorBoundary';
import { ExploreActions } from 'src/explore/actions/exploreActions';
@ -43,6 +44,13 @@ export type ControlProps = {
renderTrigger?: boolean;
};
/**
*
*/
export type ControlComponentProps<
ValueType extends JsonValue = JsonValue
> = Omit<ControlProps, 'value'> & BaseControlComponentProps<ValueType>;
export default function Control(props: ControlProps) {
const {
actions: { setControlValue },

View File

@ -26,8 +26,8 @@ import Icons from 'src/components/Icons';
const propTypes = {
name: PropTypes.string,
label: PropTypes.string,
description: PropTypes.string,
label: PropTypes.node,
description: PropTypes.node,
validationErrors: PropTypes.array,
renderTrigger: PropTypes.bool,
rightNode: PropTypes.node,

View File

@ -45,7 +45,7 @@ const datasource = {
datasource_name: 'table1',
description: 'desc',
};
const props = {
const props: DatasourcePanelProps = {
datasource,
controls: {
datasource: {
@ -57,12 +57,7 @@ const props = {
},
},
actions: {
setControlValue: () => ({
type: 'type',
controlName: 'control',
value: 'val',
validationErrors: [],
}),
setControlValue: jest.fn(),
},
};

View File

@ -16,13 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { ControlConfig, DatasourceMeta } from '@superset-ui/chart-controls';
import { debounce } from 'lodash';
import { matchSorter, rankings } from 'match-sorter';
@ -193,60 +187,61 @@ export default function DataSourcePanel({
const DEFAULT_MAX_COLUMNS_LENGTH = 50;
const DEFAULT_MAX_METRICS_LENGTH = 50;
const search = useCallback(
debounce((value: string) => {
if (value === '') {
setList({ columns, metrics });
return;
}
setList({
columns: matchSorter(columns, value, {
keys: [
{
key: 'verbose_name',
threshold: rankings.CONTAINS,
},
{
key: 'column_name',
threshold: rankings.CONTAINS,
},
{
key: item =>
[item.description, item.expression].map(
x => x?.replace(/[_\n\s]+/g, ' ') || '',
),
threshold: rankings.CONTAINS,
maxRanking: rankings.CONTAINS,
},
],
keepDiacritics: true,
}),
metrics: matchSorter(metrics, value, {
keys: [
{
key: 'verbose_name',
threshold: rankings.CONTAINS,
},
{
key: 'metric_name',
threshold: rankings.CONTAINS,
},
{
key: item =>
[item.description, item.expression].map(
x => x?.replace(/[_\n\s]+/g, ' ') || '',
),
threshold: rankings.CONTAINS,
maxRanking: rankings.CONTAINS,
},
],
keepDiacritics: true,
baseSort: (a, b) =>
Number(b.item.is_certified) - Number(a.item.is_certified) ||
String(a.rankedValue).localeCompare(b.rankedValue),
}),
});
}, FAST_DEBOUNCE),
const search = useMemo(
() =>
debounce((value: string) => {
if (value === '') {
setList({ columns, metrics });
return;
}
setList({
columns: matchSorter(columns, value, {
keys: [
{
key: 'verbose_name',
threshold: rankings.CONTAINS,
},
{
key: 'column_name',
threshold: rankings.CONTAINS,
},
{
key: item =>
[item.description, item.expression].map(
x => x?.replace(/[_\n\s]+/g, ' ') || '',
),
threshold: rankings.CONTAINS,
maxRanking: rankings.CONTAINS,
},
],
keepDiacritics: true,
}),
metrics: matchSorter(metrics, value, {
keys: [
{
key: 'verbose_name',
threshold: rankings.CONTAINS,
},
{
key: 'metric_name',
threshold: rankings.CONTAINS,
},
{
key: item =>
[item.description, item.expression].map(
x => x?.replace(/[_\n\s]+/g, ' ') || '',
),
threshold: rankings.CONTAINS,
maxRanking: rankings.CONTAINS,
},
],
keepDiacritics: true,
baseSort: (a, b) =>
Number(b.item.is_certified) - Number(a.item.is_certified) ||
String(a.rankedValue).localeCompare(b.rankedValue),
}),
});
}, FAST_DEBOUNCE),
[columns, metrics],
);

View File

@ -18,13 +18,17 @@
*/
import React from 'react';
import { render, screen } from 'spec/helpers/testing-library';
import { LabelProps } from 'src/explore/components/controls/DndColumnSelectControl/types';
import { DndColumnSelect } from 'src/explore/components/controls/DndColumnSelectControl/DndColumnSelect';
import {
DndColumnSelect,
DndColumnSelectProps,
} from 'src/explore/components/controls/DndColumnSelectControl/DndColumnSelect';
const defaultProps: LabelProps = {
const defaultProps: DndColumnSelectProps = {
type: 'DndColumnSelect',
name: 'Filter',
onChange: jest.fn(),
options: { string: { column_name: 'Column A' } },
actions: { setControlValue: jest.fn() },
};
test('renders with default props', () => {

View File

@ -20,7 +20,6 @@ import React, { useCallback, useMemo, useState } from 'react';
import { FeatureFlag, isFeatureEnabled, tn } from '@superset-ui/core';
import { ColumnMeta } from '@superset-ui/chart-controls';
import { isEmpty } from 'lodash';
import { LabelProps } from 'src/explore/components/controls/DndColumnSelectControl/types';
import DndSelectLabel from 'src/explore/components/controls/DndColumnSelectControl/DndSelectLabel';
import OptionWrapper from 'src/explore/components/controls/DndColumnSelectControl/OptionWrapper';
import { OptionSelector } from 'src/explore/components/controls/DndColumnSelectControl/utils';
@ -28,8 +27,13 @@ import { DatasourcePanelDndItem } from 'src/explore/components/DatasourcePanel/t
import { DndItemType } from 'src/explore/components/DndItemType';
import { useComponentDidUpdate } from 'src/common/hooks/useComponentDidUpdate';
import ColumnSelectPopoverTrigger from './ColumnSelectPopoverTrigger';
import { DndControlProps } from './types';
export const DndColumnSelect = (props: LabelProps) => {
export type DndColumnSelectProps = DndControlProps<string> & {
options: Record<string, ColumnMeta>;
};
export function DndColumnSelect(props: DndColumnSelectProps) {
const {
value,
options,
@ -68,6 +72,7 @@ export const DndColumnSelect = (props: LabelProps) => {
) {
onChange(optionSelectorValues);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [JSON.stringify(value), JSON.stringify(optionSelector.getValues())]);
// useComponentDidUpdate to avoid running this for the first render, to avoid
@ -203,7 +208,7 @@ export const DndColumnSelect = (props: LabelProps) => {
return (
<div>
<DndSelectLabel<string | string[], ColumnMeta[]>
<DndSelectLabel
onDrop={onDrop}
canDrop={canDrop}
valuesRenderer={valuesRenderer}
@ -229,4 +234,4 @@ export const DndColumnSelect = (props: LabelProps) => {
</ColumnSelectPopoverTrigger>
</div>
);
};
}

View File

@ -23,17 +23,24 @@ import AdhocMetric from 'src/explore/components/controls/MetricControl/AdhocMetr
import AdhocFilter, {
EXPRESSION_TYPES,
} from 'src/explore/components/controls/FilterControl/AdhocFilter';
import { DndFilterSelect } from 'src/explore/components/controls/DndColumnSelectControl/DndFilterSelect';
import {
DndFilterSelect,
DndFilterSelectProps,
} from 'src/explore/components/controls/DndColumnSelectControl/DndFilterSelect';
import { PLACEHOLDER_DATASOURCE } from 'src/dashboard/constants';
import { DEFAULT_FORM_DATA } from '@superset-ui/plugin-chart-echarts/lib/Timeseries/types';
const defaultProps = {
const defaultProps: DndFilterSelectProps = {
type: 'DndFilterSelect',
name: 'Filter',
value: [],
columns: [],
datasource: {},
formData: {},
datasource: PLACEHOLDER_DATASOURCE,
formData: null,
savedMetrics: [],
selectedMetrics: [],
onChange: jest.fn(),
options: { string: { column_name: 'Column' } },
actions: { setControlValue: jest.fn() },
};
test('renders with default props', () => {
@ -53,9 +60,15 @@ test('renders with value', () => {
});
test('renders options with saved metric', () => {
render(<DndFilterSelect {...defaultProps} formData={['saved_metric']} />, {
useDnd: true,
});
render(
<DndFilterSelect
{...defaultProps}
formData={{ ...DEFAULT_FORM_DATA, metrics: ['saved_metric'] }}
/>,
{
useDnd: true,
},
);
expect(screen.getByText('Drop columns or metrics here')).toBeInTheDocument();
});
@ -84,8 +97,14 @@ test('renders options with adhoc metric', () => {
expression: 'AVG(birth_names.num)',
metric_name: 'avg__num',
});
render(<DndFilterSelect {...defaultProps} formData={[adhocMetric]} />, {
useDnd: true,
});
render(
<DndFilterSelect
{...defaultProps}
formData={{ ...DEFAULT_FORM_DATA, metrics: [adhocMetric] }}
/>,
{
useDnd: true,
},
);
expect(screen.getByText('Drop columns or metrics here')).toBeInTheDocument();
});

View File

@ -22,6 +22,8 @@ import {
isFeatureEnabled,
logging,
Metric,
QueryFormData,
QueryFormMetric,
SupersetClient,
t,
} from '@superset-ui/core';
@ -30,11 +32,8 @@ import {
OPERATOR_ENUM_TO_OPERATOR_TYPE,
Operators,
} from 'src/explore/constants';
import { OptionSortType } from 'src/explore/types';
import {
DndFilterSelectProps,
OptionValueType,
} from 'src/explore/components/controls/DndColumnSelectControl/types';
import { Datasource, OptionSortType } from 'src/explore/types';
import { OptionValueType } from 'src/explore/components/controls/DndColumnSelectControl/types';
import AdhocFilterPopoverTrigger from 'src/explore/components/controls/FilterControl/AdhocFilterPopoverTrigger';
import OptionWrapper from 'src/explore/components/controls/DndColumnSelectControl/OptionWrapper';
import DndSelectLabel from 'src/explore/components/controls/DndColumnSelectControl/DndSelectLabel';
@ -48,6 +47,7 @@ import {
DndItemValue,
} from 'src/explore/components/DatasourcePanel/types';
import { DndItemType } from 'src/explore/components/DndItemType';
import { ControlComponentProps } from 'src/explore/components/Control';
const DND_ACCEPTED_TYPES = [
DndItemType.Column,
@ -59,8 +59,16 @@ const DND_ACCEPTED_TYPES = [
const isDictionaryForAdhocFilter = (value: OptionValueType) =>
!(value instanceof AdhocFilter) && value?.expressionType;
export interface DndFilterSelectProps
extends ControlComponentProps<OptionValueType[]> {
columns: ColumnMeta[];
savedMetrics: Metric[];
selectedMetrics: QueryFormMetric[];
datasource: Datasource;
}
export const DndFilterSelect = (props: DndFilterSelectProps) => {
const { datasource, onChange } = props;
const { datasource, onChange = () => {}, name: controlName } = props;
const propsValues = Array.from(props.value ?? []);
const [values, setValues] = useState(
@ -74,7 +82,7 @@ export const DndFilterSelect = (props: DndFilterSelectProps) => {
const optionsForSelect = (
columns: ColumnMeta[],
formData: Record<string, any>,
formData: QueryFormData | null | undefined,
) => {
const options: OptionSortType[] = [
...columns,
@ -369,7 +377,7 @@ export const DndFilterSelect = (props: DndFilterSelectProps) => {
setDroppedItem(item.value);
togglePopover(true);
},
[togglePopover],
[controlName, togglePopover],
);
const ghostButtonText = isFeatureEnabled(FeatureFlag.ENABLE_DND_WITH_CLICK_UX)
@ -378,7 +386,7 @@ export const DndFilterSelect = (props: DndFilterSelectProps) => {
return (
<>
<DndSelectLabel<OptionValueType, OptionValueType[]>
<DndSelectLabel
onDrop={handleDrop}
canDrop={canDrop}
valuesRenderer={valuesRenderer}

View File

@ -19,11 +19,13 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import {
DatasourceType,
ensureIsArray,
FeatureFlag,
GenericDataType,
isFeatureEnabled,
Metric,
QueryFormMetric,
tn,
} from '@superset-ui/core';
import { ColumnMeta } from '@superset-ui/chart-controls';
@ -32,12 +34,12 @@ import { usePrevious } from 'src/common/hooks/usePrevious';
import AdhocMetric from 'src/explore/components/controls/MetricControl/AdhocMetric';
import AdhocMetricPopoverTrigger from 'src/explore/components/controls/MetricControl/AdhocMetricPopoverTrigger';
import MetricDefinitionValue from 'src/explore/components/controls/MetricControl/MetricDefinitionValue';
import { OptionValueType } from 'src/explore/components/controls/DndColumnSelectControl/types';
import { DatasourcePanelDndItem } from 'src/explore/components/DatasourcePanel/types';
import { DndItemType } from 'src/explore/components/DndItemType';
import DndSelectLabel from 'src/explore/components/controls/DndColumnSelectControl/DndSelectLabel';
import { savedMetricType } from 'src/explore/components/controls/MetricControl/types';
import { AGGREGATES } from 'src/explore/constants';
import { DndControlProps } from './types';
const EMPTY_OBJECT = {};
const DND_ACCEPTED_TYPES = [DndItemType.Column, DndItemType.Metric];
@ -75,10 +77,12 @@ const getOptionsForSavedMetrics = (
: savedMetric,
) ?? [];
type ValueType = Metric | AdhocMetric | QueryFormMetric;
const columnsContainAllMetrics = (
value: (string | AdhocMetric | ColumnMeta)[],
value: ValueType | ValueType[] | null | undefined,
columns: ColumnMeta[],
savedMetrics: savedMetricType[],
savedMetrics: (savedMetricType | Metric)[],
) => {
const columnNames = new Set(
[...(columns || []), ...(savedMetrics || [])]
@ -104,6 +108,12 @@ const columnsContainAllMetrics = (
);
};
export type DndMetricSelectProps = DndControlProps<ValueType> & {
savedMetrics: savedMetricType[];
columns: ColumnMeta[];
datasourceType?: DatasourceType;
};
export const DndMetricSelect = (props: any) => {
const { onChange, multi, columns, savedMetrics } = props;
@ -130,7 +140,7 @@ export const DndMetricSelect = (props: any) => {
[multi, onChange],
);
const [value, setValue] = useState<(AdhocMetric | Metric | string)[]>(
const [value, setValue] = useState<ValueType[]>(
coerceAdhocMetrics(props.value),
);
const [droppedItem, setDroppedItem] = useState<DatasourcePanelDndItem | null>(
@ -176,7 +186,9 @@ export const DndMetricSelect = (props: any) => {
const onNewMetric = useCallback(
(newMetric: Metric) => {
const newValue = props.multi ? [...value, newMetric] : [newMetric];
const newValue = props.multi
? [...value, newMetric.metric_name]
: [newMetric.metric_name];
setValue(newValue);
handleChange(newValue);
},
@ -191,7 +203,7 @@ export const DndMetricSelect = (props: any) => {
const newValue = value.map(value => {
if (
// compare saved metrics
value === (oldMetric as Metric).metric_name ||
('metric_name' in oldMetric && value === oldMetric.metric_name) ||
// compare adhoc metrics
typeof (value as AdhocMetric).optionName !== 'undefined'
? (value as AdhocMetric).optionName ===
@ -254,7 +266,7 @@ export const DndMetricSelect = (props: any) => {
);
const valueRenderer = useCallback(
(option: Metric | AdhocMetric | string, index: number) => (
(option: ValueType, index: number) => (
<MetricDefinitionValue
key={index}
index={index}
@ -353,7 +365,7 @@ export const DndMetricSelect = (props: any) => {
return (
<div className="metrics-select">
<DndSelectLabel<OptionValueType, OptionValueType[]>
<DndSelectLabel
onDrop={handleDrop}
canDrop={canDrop}
valuesRenderer={valuesRenderer}

View File

@ -19,16 +19,16 @@
import React from 'react';
import { render, screen } from 'spec/helpers/testing-library';
import { DndItemType } from 'src/explore/components/DndItemType';
import DndSelectLabel from 'src/explore/components/controls/DndColumnSelectControl/DndSelectLabel';
import DndSelectLabel, {
DndSelectLabelProps,
} from 'src/explore/components/controls/DndColumnSelectControl/DndSelectLabel';
const defaultProps = {
const defaultProps: DndSelectLabelProps = {
name: 'Column',
accept: 'Column' as DndItemType,
onDrop: jest.fn(),
canDrop: () => false,
valuesRenderer: () => <span />,
onChange: jest.fn(),
options: { string: { column_name: 'Column' } },
};
test('renders with default props', async () => {

View File

@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import React, { ReactNode } from 'react';
import { useDrop } from 'react-dnd';
import { t, useTheme } from '@superset-ui/core';
import ControlHeader from 'src/explore/components/ControlHeader';
@ -25,18 +25,35 @@ import {
DndLabelsContainer,
HeaderContainer,
} from 'src/explore/components/controls/OptionControls';
import { DatasourcePanelDndItem } from 'src/explore/components/DatasourcePanel/types';
import {
DatasourcePanelDndItem,
DndItemValue,
} from 'src/explore/components/DatasourcePanel/types';
import Icons from 'src/components/Icons';
import { DndColumnSelectProps } from './types';
import { DndItemType } from '../../DndItemType';
export default function DndSelectLabel<T, O>({
export type DndSelectLabelProps = {
name: string;
accept: DndItemType | DndItemType[];
ghostButtonText?: string;
onDrop: (item: DatasourcePanelDndItem) => void;
canDrop: (item: DatasourcePanelDndItem) => boolean;
canDropValue?: (value: DndItemValue) => boolean;
onDropValue?: (value: DndItemValue) => void;
valuesRenderer: () => ReactNode;
displayGhostButton?: boolean;
onClickGhostButton?: () => void;
};
export default function DndSelectLabel({
displayGhostButton = true,
accept,
...props
}: DndColumnSelectProps<T, O>) {
}: DndSelectLabelProps) {
const theme = useTheme();
const [{ isOver, canDrop }, datasourcePanelDrop] = useDrop({
accept: props.accept,
accept,
drop: (item: DatasourcePanelDndItem) => {
props.onDrop(item);

View File

@ -17,13 +17,9 @@
* under the License.
*/
import { ReactNode } from 'react';
import { Metric } from '@superset-ui/core';
import { JsonValue } from '@superset-ui/core';
import { ControlComponentProps } from 'src/explore/components/Control';
import { ColumnMeta } from '@superset-ui/chart-controls';
import {
DatasourcePanelDndItem,
DndItemValue,
} from '../../DatasourcePanel/types';
import { DndItemType } from '../../DndItemType';
export interface OptionProps {
children?: ReactNode;
@ -42,40 +38,16 @@ export interface OptionItemInterface {
dragIndex: number;
}
export interface LabelProps<T = string[] | string> {
name: string;
value?: T;
onChange: (value?: T) => void;
options: { string: ColumnMeta };
/**
* Shared control props for all DnD control.
*/
export type DndControlProps<
ValueType extends JsonValue
> = ControlComponentProps<ValueType | ValueType[] | null> & {
multi?: boolean;
canDelete?: boolean;
ghostButtonText?: string;
label?: string;
}
export interface DndColumnSelectProps<
T = string[] | string,
O = string[] | string
> extends LabelProps<T> {
onDrop: (item: DatasourcePanelDndItem) => void;
canDrop: (item: DatasourcePanelDndItem) => boolean;
canDropValue?: (value: DndItemValue) => boolean;
onDropValue?: (value: DndItemValue) => void;
valuesRenderer: () => ReactNode;
accept: DndItemType | DndItemType[];
ghostButtonText?: string;
displayGhostButton?: boolean;
onClickGhostButton?: () => void;
}
onChange: (value: ValueType | ValueType[] | null | undefined) => void;
};
export type OptionValueType = Record<string, any>;
export interface DndFilterSelectProps {
name: string;
value: OptionValueType[];
columns: ColumnMeta[];
datasource: Record<string, any>;
formData: Record<string, any>;
savedMetrics: Metric[];
onChange: (filters: OptionValueType[]) => void;
options: { string: ColumnMeta };
}

View File

@ -22,25 +22,25 @@ import { ensureIsArray } from '@superset-ui/core';
export class OptionSelector {
values: ColumnMeta[];
options: { string: ColumnMeta };
options: Record<string, ColumnMeta>;
multi: boolean;
constructor(
options: { string: ColumnMeta },
options: Record<string, ColumnMeta>,
multi: boolean,
initialValues?: string[] | string,
initialValues?: string[] | string | null,
) {
this.options = options;
this.multi = multi;
this.values = ensureIsArray(initialValues)
.map(value => {
if (value in options) {
if (value && value in options) {
return options[value];
}
return null;
})
.filter(Boolean);
.filter(Boolean) as ColumnMeta[];
}
add(value: string) {

View File

@ -22,7 +22,8 @@ import {
AnnotationData,
AdhocMetric,
} from '@superset-ui/core';
import { ColumnMeta } from '@superset-ui/chart-controls';
import { ColumnMeta, DatasourceMeta } from '@superset-ui/chart-controls';
import { DatabaseObject } from 'src/views/CRUD/types';
export { Slice, Chart } from 'src/types/Chart';
@ -54,3 +55,10 @@ export interface ChartState {
export type OptionSortType = Partial<
ColumnMeta & AdhocMetric & { saved_metric_name: string }
>;
export type Datasource = DatasourceMeta & {
database?: DatabaseObject;
datasource?: string;
schema?: string;
is_sqllab_view?: boolean;
};

View File

@ -0,0 +1,29 @@
/**
* 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.
*/
export default interface Database {
id: number;
allow_run_async: boolean;
database_name: string;
encrypted_extra: string;
extra: string;
impersonate_user: boolean;
server_cert: string;
sqlalchemy_uri: string;
}

View File

@ -17,6 +17,7 @@
* under the License.
*/
import { User } from 'src/types/bootstrapTypes';
import Database from 'src/types/Database';
import Owner from 'src/types/Owner';
export type FavoriteStatus = {
@ -137,12 +138,5 @@ export type ImportResourceName =
| 'dataset'
| 'saved_query';
export type DatabaseObject = {
allow_run_async?: boolean;
database_name?: string;
encrypted_extra?: string;
extra?: string;
impersonate_user?: boolean;
server_cert?: string;
sqlalchemy_uri: string;
};
export type DatabaseObject = Partial<Database> &
Pick<Database, 'sqlalchemy_uri'>;