mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-11-10 10:29:12 +03:00
feature (console/data-relationships): generic relationships view table that's compatible with GDC
PR-URL: https://github.com/hasura/graphql-engine-mono/pull/5525 GitOrigin-RevId: 02bdfa11ae705db4a2e50f4696a3178797a00412
This commit is contained in:
parent
a441308938
commit
c0cff3c908
20
console/package-lock.json
generated
20
console/package-lock.json
generated
@ -43,6 +43,7 @@
|
||||
"jwt-decode": "^3.0.0",
|
||||
"less": "3.11.1",
|
||||
"lodash.get": "4.4.2",
|
||||
"lodash.isequal": "^4.5.0",
|
||||
"lodash.merge": "4.6.2",
|
||||
"lodash.pickby": "^4.6.0",
|
||||
"lodash.uniqueid": "^4.0.1",
|
||||
@ -126,6 +127,7 @@
|
||||
"@types/jquery": "3.3.33",
|
||||
"@types/jwt-decode": "2.2.1",
|
||||
"@types/lodash": "^4.14.159",
|
||||
"@types/lodash.isequal": "^4.5.6",
|
||||
"@types/lodash.merge": "^4.6.6",
|
||||
"@types/lodash.pickby": "^4.6.7",
|
||||
"@types/lodash.uniqueid": "^4.0.7",
|
||||
@ -10924,6 +10926,15 @@
|
||||
"@types/lodash": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/lodash.isequal": {
|
||||
"version": "4.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash.isequal/-/lodash.isequal-4.5.6.tgz",
|
||||
"integrity": "sha512-Ww4UGSe3DmtvLLJm2F16hDwEQSv7U0Rr8SujLUA2wHI2D2dm8kPu6Et+/y303LfjTIwSBKXB/YTUcAKpem/XEg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/lodash": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/lodash.merge": {
|
||||
"version": "4.6.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash.merge/-/lodash.merge-4.6.7.tgz",
|
||||
@ -49607,6 +49618,15 @@
|
||||
"@types/lodash": "*"
|
||||
}
|
||||
},
|
||||
"@types/lodash.isequal": {
|
||||
"version": "4.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash.isequal/-/lodash.isequal-4.5.6.tgz",
|
||||
"integrity": "sha512-Ww4UGSe3DmtvLLJm2F16hDwEQSv7U0Rr8SujLUA2wHI2D2dm8kPu6Et+/y303LfjTIwSBKXB/YTUcAKpem/XEg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/lodash": "*"
|
||||
}
|
||||
},
|
||||
"@types/lodash.merge": {
|
||||
"version": "4.6.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash.merge/-/lodash.merge-4.6.7.tgz",
|
||||
|
@ -97,6 +97,7 @@
|
||||
"jwt-decode": "^3.0.0",
|
||||
"less": "3.11.1",
|
||||
"lodash.get": "4.4.2",
|
||||
"lodash.isequal": "^4.5.0",
|
||||
"lodash.merge": "4.6.2",
|
||||
"lodash.pickby": "^4.6.0",
|
||||
"lodash.uniqueid": "^4.0.1",
|
||||
@ -180,6 +181,7 @@
|
||||
"@types/jquery": "3.3.33",
|
||||
"@types/jwt-decode": "2.2.1",
|
||||
"@types/lodash": "^4.14.159",
|
||||
"@types/lodash.isequal": "^4.5.6",
|
||||
"@types/lodash.merge": "^4.6.6",
|
||||
"@types/lodash.pickby": "^4.6.7",
|
||||
"@types/lodash.uniqueid": "^4.0.7",
|
||||
|
@ -76,7 +76,6 @@ const DataSubSidebar = props => {
|
||||
dataSources,
|
||||
sidebarLoadingState,
|
||||
currentTable,
|
||||
headers,
|
||||
} = props;
|
||||
const { setDriver } = useDataSource();
|
||||
|
||||
@ -327,7 +326,6 @@ const DataSubSidebar = props => {
|
||||
schemaLoading={schemaLoading}
|
||||
preLoadState={preLoadState}
|
||||
gdcItemClick={handleGDCTreeClick}
|
||||
headers={headers}
|
||||
/>
|
||||
</div>
|
||||
</ul>
|
||||
@ -356,7 +354,6 @@ const mapStateToProps = state => {
|
||||
pathname: state?.routing?.locationBeforeTransitions?.pathname,
|
||||
dataSources: getDataSources(state),
|
||||
sidebarLoadingState: state.dataSidebar.loading,
|
||||
headers: state.tables.dataHeaders,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -11,5 +11,5 @@ export default {
|
||||
} as ComponentMeta<typeof GDCTree>;
|
||||
|
||||
export const Primary: ComponentStory<typeof GDCTree> = args => (
|
||||
<GDCTree onSelect={args.onSelect} headers={{}} />
|
||||
<GDCTree onSelect={args.onSelect} />
|
||||
);
|
||||
|
@ -26,7 +26,6 @@ const getCurrentActiveKeys = () => {
|
||||
|
||||
type Props = {
|
||||
onSelect: (value: Key[]) => void;
|
||||
headers: Record<string, string>;
|
||||
};
|
||||
|
||||
/*
|
||||
@ -39,7 +38,7 @@ export const GDCTree = (props: Props) => {
|
||||
const isGDCRouteActive = checkForGDCRoute();
|
||||
|
||||
const activeKey = isGDCRouteActive ? getCurrentActiveKeys() : [];
|
||||
const { data: gdcDatabases } = useTreeData({ headers: props.headers });
|
||||
const { data: gdcDatabases } = useTreeData();
|
||||
|
||||
if (!gdcDatabases || gdcDatabases.length === 0) return null;
|
||||
|
||||
|
@ -2,12 +2,8 @@ import { useHttpClient } from '@/features/Network';
|
||||
import { useQuery } from 'react-query';
|
||||
import { getTreeData } from '../utils';
|
||||
|
||||
export const useTreeData = ({
|
||||
headers,
|
||||
}: {
|
||||
headers: Record<string, string>;
|
||||
}) => {
|
||||
const httpClient = useHttpClient({ headers });
|
||||
export const useTreeData = () => {
|
||||
const httpClient = useHttpClient();
|
||||
|
||||
return useQuery({
|
||||
queryKey: 'treeview',
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { Table } from '@/features/DataSource';
|
||||
|
||||
/*
|
||||
A GDC Source can be any user defined DB that can be added during run-time. We can only know a few properties during build time, such as name and kind
|
||||
which will be String, but for the tables - A GDC source can have any valid JSON definition for the `tables[i].table` property. The closest we can type is
|
||||
@ -7,6 +9,6 @@ export type GDCSource = {
|
||||
name: string;
|
||||
kind: string;
|
||||
tables: {
|
||||
table: Record<string, any>;
|
||||
table: Table;
|
||||
}[];
|
||||
};
|
||||
|
@ -42,6 +42,7 @@ import { getRemoteSchemasSelector } from '../../../../metadata/selector';
|
||||
import { RightContainer } from '../../../Common/Layout/RightContainer';
|
||||
import FeatureDisabled from '../FeatureDisabled';
|
||||
import { RemoteDbRelationships } from './RemoteDbRelationships/RemoteDbRelationships';
|
||||
import { areTablesEqual } from '@/features/RelationshipsTable/utils';
|
||||
|
||||
const addRelationshipCellView = (
|
||||
dispatch,
|
||||
@ -325,6 +326,7 @@ const Relationships = ({
|
||||
schemaList,
|
||||
readOnlyMode,
|
||||
currentSource,
|
||||
source,
|
||||
}) => {
|
||||
useEffect(() => {
|
||||
dispatch(resetRelationshipForm());
|
||||
@ -336,6 +338,17 @@ const Relationships = ({
|
||||
t => t.table_name === tableName && t.table_schema === currentSchema
|
||||
);
|
||||
|
||||
/**
|
||||
* Metadata table object - this is the "table" that needs to be passed for all our new components
|
||||
* The existing `NormalizedTable` stuff has to be phased out slowly
|
||||
* */
|
||||
const table = source.tables.find(t => {
|
||||
return areTablesEqual(t.table, {
|
||||
table_schema: tableSchema.table_schema,
|
||||
table_name: tableSchema.table_name,
|
||||
});
|
||||
});
|
||||
|
||||
const { data: featureFlagsData, isLoading: isFeatureFlagsLoading } =
|
||||
useFeatureFlags();
|
||||
if (isFeatureFlagsLoading) return <div>Loading...</div>;
|
||||
@ -367,6 +380,7 @@ const Relationships = ({
|
||||
return (
|
||||
<DatabaseRelationshipsTab
|
||||
table={tableSchema}
|
||||
metadataTable={table.table}
|
||||
driver={currentDriver}
|
||||
currentSource={currentSource}
|
||||
migrationMode={migrationMode}
|
||||
@ -601,6 +615,9 @@ const mapStateToProps = (state, ownProps) => {
|
||||
remoteSchemas: getRemoteSchemasSelector(state).map(schema => schema.name),
|
||||
adminHeaders: state.tables.dataHeaders,
|
||||
currentSource: state.tables.currentDataSource,
|
||||
source: state.metadata.metadataObject.sources.find(
|
||||
s => s.name === state.tables.currentDataSource
|
||||
),
|
||||
...state.tables.modify,
|
||||
};
|
||||
};
|
||||
|
@ -388,7 +388,6 @@ type TreeViewProps = {
|
||||
schemaLoading: boolean;
|
||||
preLoadState: boolean;
|
||||
gdcItemClick: (value: Key[]) => void;
|
||||
headers: any;
|
||||
};
|
||||
const TreeView: React.FC<TreeViewProps> = ({
|
||||
items,
|
||||
@ -401,7 +400,6 @@ const TreeView: React.FC<TreeViewProps> = ({
|
||||
schemaLoading,
|
||||
preLoadState,
|
||||
gdcItemClick,
|
||||
headers,
|
||||
}) => {
|
||||
const handleSelectDataSource = (dataSource: string) => {
|
||||
onDatabaseChange(dataSource);
|
||||
@ -453,7 +451,7 @@ const TreeView: React.FC<TreeViewProps> = ({
|
||||
))}
|
||||
{GDC_TREE_VIEW_DEV === 'enabled' ? (
|
||||
<div id="tree-container" className="inline-block">
|
||||
<GDCTree onSelect={gdcItemClick} headers={headers} />
|
||||
<GDCTree onSelect={gdcItemClick} />
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
@ -29,6 +29,7 @@ const table: NormalizedTable = {
|
||||
export const Primary = () => (
|
||||
<DatabaseRelationshipsTab
|
||||
table={table}
|
||||
metadataTable={{ name: 'user', schema: 'public' }}
|
||||
currentSource="default"
|
||||
migrationMode
|
||||
driver="postgres"
|
||||
|
@ -2,57 +2,24 @@ import React from 'react';
|
||||
import { RightContainer } from '@/components/Common/Layout/RightContainer';
|
||||
import { Button } from '@/new-components/Button';
|
||||
import { useFireNotification } from '@/new-components/Notifications';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import { getConfirmation } from '@/components/Common/utils/jsUtils';
|
||||
import { Driver } from '@/dataSources';
|
||||
import { FaPlusCircle } from 'react-icons/fa';
|
||||
|
||||
import { NormalizedTable } from '@/dataSources/types';
|
||||
import { DataSourceDriver, getDataSourcePrefix } from '@/metadata/queryUtils';
|
||||
|
||||
import TableHeader from '../../components/Services/Data/TableCommon/TableHeader';
|
||||
import { FeatureFlagFloatingButton } from '../FeatureFlags/components/FeatureFlagFloatingButton';
|
||||
import {
|
||||
DatabaseRelationshipsTable,
|
||||
OnClickHandlerArgs,
|
||||
RowData,
|
||||
} from '../RelationshipsTable/DatabaseRelationshipsTable';
|
||||
import {
|
||||
allowedMetadataTypes,
|
||||
DbToDbRelationship,
|
||||
DbToRemoteSchemaRelationship,
|
||||
useMetadataMigration,
|
||||
} from '../MetadataAPI';
|
||||
import { DataTarget } from '../Datasources';
|
||||
import { DatabaseRelationshipsTable } from '../RelationshipsTable/DatabaseRelationshipsTable';
|
||||
import { allowedMetadataTypes, useMetadataMigration } from '../MetadataAPI';
|
||||
import { Form } from './components/Form/Form';
|
||||
|
||||
const createTable = (target?: DataTarget) => {
|
||||
if (!target) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if ('schema' in target) {
|
||||
return {
|
||||
name: target.table,
|
||||
schema: target.schema,
|
||||
};
|
||||
}
|
||||
|
||||
if ('dataset' in target) {
|
||||
return {
|
||||
name: target.table,
|
||||
schema: target.dataset,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
name: target.table,
|
||||
};
|
||||
};
|
||||
import { Relationship } from '../RelationshipsTable/DatabaseRelationshipsTable/types';
|
||||
import { Table } from '../DataSource';
|
||||
|
||||
const useFormState = (currentSource: string) => {
|
||||
const [isOpen, setIsOpen] = React.useState(false);
|
||||
const [existingRelationship, setExistingRelationship] =
|
||||
React.useState<RowData>();
|
||||
React.useState<Relationship>();
|
||||
|
||||
const { fireNotification } = useFireNotification();
|
||||
const mutation = useMetadataMigration({
|
||||
@ -77,16 +44,17 @@ const useFormState = (currentSource: string) => {
|
||||
setIsOpen(true);
|
||||
};
|
||||
|
||||
const onEdit = (row?: RowData) => {
|
||||
setExistingRelationship(row);
|
||||
const editRelationship = (relationship: Relationship) => {
|
||||
setExistingRelationship(relationship);
|
||||
setIsOpen(true);
|
||||
};
|
||||
const onCancel = () => {
|
||||
|
||||
const closeForm = () => {
|
||||
setExistingRelationship(undefined);
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
const onDelete = (row?: RowData) => {
|
||||
const deleteRelationship = (row: Relationship) => {
|
||||
setExistingRelationship(undefined);
|
||||
setIsOpen(false);
|
||||
const confirmMessage = `This will permanently delete the ${row?.name} from Hasura`;
|
||||
@ -97,66 +65,70 @@ const useFormState = (currentSource: string) => {
|
||||
|
||||
const sourcePrefix = getDataSourcePrefix(currentSource as DataSourceDriver);
|
||||
|
||||
if (row?.toType === 'remote_schema') {
|
||||
return mutation.mutate({
|
||||
if (row.type === 'toRemoteSchema') {
|
||||
mutation.mutate({
|
||||
query: {
|
||||
type: `${sourcePrefix}delete_remote_relationship` as allowedMetadataTypes,
|
||||
args: {
|
||||
name: row?.name,
|
||||
name: row.name,
|
||||
source: currentSource,
|
||||
table: createTable(
|
||||
(row?.relationship as DbToRemoteSchemaRelationship)?.target
|
||||
),
|
||||
table: row.mapping.from.table,
|
||||
},
|
||||
},
|
||||
});
|
||||
} else if (row?.toType === 'database') {
|
||||
return mutation.mutate({
|
||||
return;
|
||||
}
|
||||
|
||||
if (row.type === 'toSource') {
|
||||
mutation.mutate({
|
||||
query: {
|
||||
type: `${sourcePrefix}delete_remote_relationship` as allowedMetadataTypes,
|
||||
args: {
|
||||
name: row?.name,
|
||||
name: row.name,
|
||||
source: currentSource,
|
||||
table: createTable(
|
||||
(row?.relationship as DbToDbRelationship)?.target
|
||||
),
|
||||
table: row.mapping.from.table,
|
||||
},
|
||||
},
|
||||
});
|
||||
} else if (row?.toType === 'table') {
|
||||
return mutation.mutate({
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* It must be a local/self table relationship
|
||||
*/
|
||||
mutation.mutate({
|
||||
query: {
|
||||
type: `${sourcePrefix}drop_relationship` as allowedMetadataTypes,
|
||||
args: {
|
||||
relationship: row?.name,
|
||||
table: row.referenceTable,
|
||||
source: row.reference,
|
||||
relationship: row.name,
|
||||
table: row.toLocalTable,
|
||||
source: currentSource,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onClick = ({ type, row }: OnClickHandlerArgs) => {
|
||||
const openForm = () => onOpen();
|
||||
|
||||
const onClick = ({ type, row }: { type: string; row: Relationship }) => {
|
||||
switch (type) {
|
||||
case 'add':
|
||||
onOpen();
|
||||
break;
|
||||
case 'close':
|
||||
onCancel();
|
||||
break;
|
||||
case 'edit':
|
||||
onEdit(row);
|
||||
break;
|
||||
case 'delete':
|
||||
onDelete(row);
|
||||
deleteRelationship(row);
|
||||
break;
|
||||
default:
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
return { isOpen, onClick, existingRelationship };
|
||||
return {
|
||||
isOpen,
|
||||
onClick,
|
||||
existingRelationship,
|
||||
openForm,
|
||||
closeForm,
|
||||
deleteRelationship,
|
||||
editRelationship,
|
||||
};
|
||||
};
|
||||
|
||||
export const DatabaseRelationshipsTab = ({
|
||||
@ -164,13 +136,25 @@ export const DatabaseRelationshipsTab = ({
|
||||
currentSource,
|
||||
migrationMode,
|
||||
driver,
|
||||
metadataTable,
|
||||
}: {
|
||||
table: NormalizedTable;
|
||||
currentSource: string;
|
||||
migrationMode: boolean;
|
||||
driver: Driver;
|
||||
metadataTable: Table;
|
||||
}) => {
|
||||
const { isOpen, existingRelationship, onClick } = useFormState(currentSource);
|
||||
const {
|
||||
isOpen,
|
||||
existingRelationship,
|
||||
editRelationship,
|
||||
openForm,
|
||||
closeForm,
|
||||
deleteRelationship,
|
||||
} = useFormState(currentSource);
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const onComplete = ({
|
||||
type,
|
||||
}: {
|
||||
@ -179,7 +163,10 @@ export const DatabaseRelationshipsTab = ({
|
||||
type: 'success' | 'error' | 'cancel';
|
||||
}) => {
|
||||
if (type === 'success' || type === 'cancel') {
|
||||
onClick({ type: 'close' });
|
||||
closeForm();
|
||||
queryClient.refetchQueries([currentSource, 'list_all_relationships'], {
|
||||
exact: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@ -195,31 +182,29 @@ export const DatabaseRelationshipsTab = ({
|
||||
count={null}
|
||||
isCountEstimated
|
||||
/>
|
||||
|
||||
<div className="py-4">
|
||||
<h2 className="text-md font-semibold">Data Relationships</h2>
|
||||
</div>
|
||||
|
||||
<DatabaseRelationshipsTable
|
||||
target={
|
||||
{
|
||||
database: currentSource,
|
||||
table: table.table_name,
|
||||
[driver === 'bigquery' ? 'dataset' : 'schema']: table.table_schema, // TODO find a better way to handle this so that GDC can work
|
||||
kind: driver,
|
||||
} as DataTarget
|
||||
}
|
||||
onClick={onClick}
|
||||
dataSourceName={currentSource}
|
||||
table={metadataTable}
|
||||
onEditRow={({ relationship }) => {
|
||||
editRelationship(relationship);
|
||||
}}
|
||||
onDeleteRow={({ relationship }) => {
|
||||
deleteRelationship(relationship);
|
||||
}}
|
||||
/>
|
||||
|
||||
{isOpen ? null : (
|
||||
<Button
|
||||
onClick={() => onClick({ type: 'add' })}
|
||||
icon={<FaPlusCircle />}
|
||||
>
|
||||
<Button onClick={() => openForm()} icon={<FaPlusCircle />}>
|
||||
Add New Relationship
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{isOpen && (
|
||||
{isOpen ? (
|
||||
<Form
|
||||
existingRelationship={existingRelationship}
|
||||
sourceTableInfo={{
|
||||
@ -230,7 +215,7 @@ export const DatabaseRelationshipsTab = ({
|
||||
onComplete={onComplete}
|
||||
driver={driver}
|
||||
/>
|
||||
)}
|
||||
) : null}
|
||||
<FeatureFlagFloatingButton />
|
||||
</RightContainer>
|
||||
);
|
||||
|
@ -1,28 +1,23 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Driver } from '@/dataSources';
|
||||
import { DataTarget } from '@/features/Datasources';
|
||||
import { DbToRsForm } from '@/features/RemoteRelationships';
|
||||
import { RowData } from '@/features/RelationshipsTable';
|
||||
import { DbToRemoteSchemaRelationship } from '@/features/MetadataAPI';
|
||||
|
||||
import {
|
||||
useFindRelationship,
|
||||
Relationship,
|
||||
} from '@/features/RelationshipsTable';
|
||||
import {
|
||||
isLegacyRemoteSchemaRelationship,
|
||||
isRemoteDBRelationship,
|
||||
isRemoteSchemaRelationship,
|
||||
} from '@/features/DataSource';
|
||||
import { RemoteDBRelationshipWidget } from '../RemoteDBRelationshipWidget';
|
||||
import { LocalRelationshipWidget } from '../LocalDBRelationshipWidget';
|
||||
import { RelOption } from './utils';
|
||||
|
||||
const getExistingRelationshipType = (
|
||||
existingRelationship: RowData
|
||||
): RelOption => {
|
||||
if (existingRelationship.toType === 'database') return 'remoteDatabase';
|
||||
if (existingRelationship.toType === 'remote_schema') return 'remoteSchema';
|
||||
|
||||
return 'local';
|
||||
};
|
||||
|
||||
type EditRelationshipFormProps = {
|
||||
driver: Driver;
|
||||
sourceTableInfo: DataTarget;
|
||||
existingRelationship: RowData;
|
||||
existingRelationship: Relationship;
|
||||
/**
|
||||
* optional callback function, can be used to get the onComplete event, this could be a onSuccess, or onError event.
|
||||
*
|
||||
@ -40,58 +35,81 @@ export const EditRelationshipForm = ({
|
||||
existingRelationship,
|
||||
onComplete,
|
||||
}: EditRelationshipFormProps) => {
|
||||
const option = React.useMemo(
|
||||
() => getExistingRelationshipType(existingRelationship),
|
||||
[existingRelationship]
|
||||
);
|
||||
const { data: relationship, isLoading } = useFindRelationship({
|
||||
dataSourceName: existingRelationship.mapping.from.source,
|
||||
table: existingRelationship.mapping.from.table,
|
||||
relationshipName: existingRelationship.name,
|
||||
});
|
||||
if (isLoading) return <>Loading...</>;
|
||||
|
||||
switch (option) {
|
||||
case 'local':
|
||||
return (
|
||||
<LocalRelationshipWidget
|
||||
key={existingRelationship.name}
|
||||
driver={driver}
|
||||
sourceTableInfo={sourceTableInfo}
|
||||
existingRelationshipName={existingRelationship.name}
|
||||
onComplete={onComplete}
|
||||
/>
|
||||
);
|
||||
case 'remoteDatabase':
|
||||
if (!relationship) return <>Relationship Not found in metadata</>;
|
||||
|
||||
if (isRemoteDBRelationship(relationship)) {
|
||||
return (
|
||||
<RemoteDBRelationshipWidget
|
||||
key={existingRelationship.name}
|
||||
key={relationship.name}
|
||||
sourceTableInfo={sourceTableInfo}
|
||||
existingRelationshipName={existingRelationship.name}
|
||||
existingRelationshipName={relationship.name}
|
||||
onComplete={onComplete}
|
||||
/>
|
||||
);
|
||||
case 'remoteSchema':
|
||||
// asserting type because here RS relationship is the only possibility, DB relationships are filtered out above with the help of `option`
|
||||
// existing relationship would handle the legacy/ new shape,
|
||||
// ie. lhs_fields will always be present (legacy hasura_field is already translated to the new format)
|
||||
const { lhs_fields, remote_field, remoteSchemaName } =
|
||||
existingRelationship.relationship as DbToRemoteSchemaRelationship;
|
||||
}
|
||||
|
||||
if (isRemoteSchemaRelationship(relationship)) {
|
||||
const { lhs_fields, remote_field, remote_schema } = {
|
||||
...relationship.definition.to_remote_schema,
|
||||
};
|
||||
|
||||
return (
|
||||
<DbToRsForm
|
||||
key={existingRelationship.name}
|
||||
key={relationship.name}
|
||||
sourceTableInfo={sourceTableInfo}
|
||||
onComplete={onComplete}
|
||||
selectedRelationship={{
|
||||
name: existingRelationship.name,
|
||||
name: relationship.name,
|
||||
definition: {
|
||||
to_remote_schema: {
|
||||
lhs_fields,
|
||||
remote_field,
|
||||
remote_schema: remoteSchemaName,
|
||||
remote_schema,
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
// This is a TS protection that forces the developer to properly manage all the cases.
|
||||
// It throws when the developer adds new values to RelOption without adding a corresponding `case` here.
|
||||
throw new Error(`Unknown RelOption: ${option}`);
|
||||
}
|
||||
|
||||
if (isLegacyRemoteSchemaRelationship(relationship)) {
|
||||
const { lhs_fields, remote_field, remote_schema } = {
|
||||
...relationship.definition,
|
||||
lhs_fields: relationship.definition.hasura_fields,
|
||||
};
|
||||
|
||||
return (
|
||||
<DbToRsForm
|
||||
key={relationship.name}
|
||||
sourceTableInfo={sourceTableInfo}
|
||||
onComplete={onComplete}
|
||||
selectedRelationship={{
|
||||
name: relationship.name,
|
||||
definition: {
|
||||
to_remote_schema: {
|
||||
lhs_fields,
|
||||
remote_field,
|
||||
remote_schema,
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<LocalRelationshipWidget
|
||||
key={relationship.name}
|
||||
driver={driver}
|
||||
sourceTableInfo={sourceTableInfo}
|
||||
existingRelationshipName={relationship.name}
|
||||
onComplete={onComplete}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -24,23 +24,29 @@ export const WithExistingRelationship: ComponentStory<typeof Form> = args => (
|
||||
);
|
||||
WithExistingRelationship.args = {
|
||||
existingRelationship: {
|
||||
fromType: 'table',
|
||||
toType: 'table',
|
||||
name: 'products',
|
||||
reference: 'default',
|
||||
referenceTable: 'user',
|
||||
target: 'default',
|
||||
targetTable: 'product',
|
||||
type: 'Object',
|
||||
fieldsFrom: ['id'],
|
||||
fieldsTo: ['fk_user_id'],
|
||||
relationship: {
|
||||
name: 'products',
|
||||
using: {
|
||||
manual_configuration: {
|
||||
remote_table: 'product',
|
||||
column_mapping: { id: 'fk_user_id' },
|
||||
name: 'Customers',
|
||||
type: 'toLocalTableFk',
|
||||
toLocalTable: {
|
||||
name: 'Customer',
|
||||
schema: 'public',
|
||||
},
|
||||
relationship_type: 'Array',
|
||||
mapping: {
|
||||
from: {
|
||||
source: 'chinook',
|
||||
table: {
|
||||
name: 'Employee',
|
||||
schema: 'public',
|
||||
},
|
||||
columns: ['SupportRepId'],
|
||||
},
|
||||
to: {
|
||||
source: 'chinook',
|
||||
table: {
|
||||
name: 'Customer',
|
||||
schema: 'public',
|
||||
},
|
||||
columns: ['EmployeeId'],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -1,15 +1,14 @@
|
||||
import React from 'react';
|
||||
|
||||
import { RowData } from '@/features/RelationshipsTable';
|
||||
import { DataTarget } from '@/features/Datasources';
|
||||
import { Driver } from '@/dataSources';
|
||||
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { Relationship } from '@/features/RelationshipsTable/DatabaseRelationshipsTable/types';
|
||||
import { FormLayout } from './FormLayout';
|
||||
import { CreateRelationshipForm } from './CreateRelationshipForm';
|
||||
import { EditRelationshipForm } from './EditRelationshipForm';
|
||||
|
||||
interface Props {
|
||||
existingRelationship?: RowData;
|
||||
existingRelationship?: Relationship;
|
||||
sourceTableInfo: DataTarget;
|
||||
driver: Driver;
|
||||
/**
|
||||
|
@ -13,5 +13,6 @@ export const bigquery: Database = {
|
||||
return ['dataset', 'name'];
|
||||
},
|
||||
getTableColumns,
|
||||
getFKRelationships: async () => Feature.NotImplemented,
|
||||
},
|
||||
};
|
||||
|
@ -2,7 +2,7 @@ import { Database, Feature } from '..';
|
||||
import { runSQL } from '../api';
|
||||
import { adaptIntrospectedTables } from '../common/utils';
|
||||
import { GetTrackableTablesProps } from '../types';
|
||||
import { getTableColumns } from './introspection';
|
||||
import { getTableColumns, getFKRelationships } from './introspection';
|
||||
|
||||
export type CitusTable = { name: string; schema: string };
|
||||
|
||||
@ -48,5 +48,6 @@ export const citus: Database = {
|
||||
return ['schema', 'name'];
|
||||
},
|
||||
getTableColumns,
|
||||
getFKRelationships,
|
||||
},
|
||||
};
|
||||
|
@ -0,0 +1,65 @@
|
||||
import { CitusTable } from '..';
|
||||
import { runSQL, RunSQLResponse } from '../../api';
|
||||
import { GetFKRelationshipProps } from '../../types';
|
||||
|
||||
const adaptFkRelationships = (
|
||||
result: RunSQLResponse['result'],
|
||||
schema: string
|
||||
) => {
|
||||
if (!result) return [];
|
||||
|
||||
return result.slice(1).map(row => ({
|
||||
from: {
|
||||
/**
|
||||
* This is to remove the schema name from tables that are not from `public` schema
|
||||
*/
|
||||
table: row[0]?.replace(/"/g, '').replace(`${schema}.`, ''),
|
||||
/**
|
||||
* break complex fk joins into array of string and remove and `"` character in the names
|
||||
*/
|
||||
column: row[1].split(',')?.map(i => i?.replace(/"/g, '')),
|
||||
},
|
||||
to: {
|
||||
table: row[2]?.replace(/"/g, '').replace(`${schema}.`, ''),
|
||||
column: row[3].split(',')?.map(i => i?.replace(/"/g, '')),
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
export const getFKRelationships = async ({
|
||||
dataSourceName,
|
||||
table,
|
||||
httpClient,
|
||||
}: GetFKRelationshipProps) => {
|
||||
const { schema, name } = table as CitusTable;
|
||||
|
||||
/**
|
||||
* This SQL goes through the pg_constraint (https://www.postgresql.org/docs/current/catalog-pg-constraint.html) and the pg_namespace (https://www.postgresql.org/docs/14/catalog-pg-namespace.html)
|
||||
* and links fk_constraint object that have the same Object ID on both tables and finally
|
||||
* delivers the list of all possible FKConstraints for a given table.
|
||||
* The contype checks basically checks for - f = foreign key constraint, p = primary key constraint,
|
||||
*/
|
||||
const sql = `SELECT conrelid::regclass AS "source_table"
|
||||
,CASE WHEN pg_get_constraintdef(c.oid) LIKE 'FOREIGN KEY %' THEN substring(pg_get_constraintdef(c.oid), 14, position(')' in pg_get_constraintdef(c.oid))-14) END AS "source_column"
|
||||
,CASE WHEN pg_get_constraintdef(c.oid) LIKE 'FOREIGN KEY %' THEN substring(pg_get_constraintdef(c.oid), position(' REFERENCES ' in pg_get_constraintdef(c.oid))+12, position('(' in substring(pg_get_constraintdef(c.oid), 14))-position(' REFERENCES ' in pg_get_constraintdef(c.oid))+1) END AS "target_table"
|
||||
,CASE WHEN pg_get_constraintdef(c.oid) LIKE 'FOREIGN KEY %' THEN substring(pg_get_constraintdef(c.oid), position('(' in substring(pg_get_constraintdef(c.oid), 14))+14, position(')' in substring(pg_get_constraintdef(c.oid), position('(' in substring(pg_get_constraintdef(c.oid), 14))+14))-1) END AS "target_column"
|
||||
FROM pg_constraint c
|
||||
JOIN pg_namespace n ON n.oid = c.connamespace
|
||||
WHERE contype IN ('f', 'p')
|
||||
AND (conrelid::regclass = '"${schema}"."${name}"'::regclass OR substring(pg_get_constraintdef(c.oid), position(' REFERENCES ' in pg_get_constraintdef(c.oid))+12, position('(' in substring(pg_get_constraintdef(c.oid), 14))-position(' REFERENCES ' in pg_get_constraintdef(c.oid))+1) = '${
|
||||
schema === 'public' ? `"${name}"` : `${schema}.${name}`
|
||||
}')
|
||||
AND pg_get_constraintdef(c.oid) LIKE 'FOREIGN KEY %';
|
||||
`;
|
||||
|
||||
const response = await runSQL({
|
||||
source: {
|
||||
name: dataSourceName,
|
||||
kind: 'postgres',
|
||||
},
|
||||
sql,
|
||||
httpClient,
|
||||
});
|
||||
|
||||
return adaptFkRelationships(response.result, schema);
|
||||
};
|
@ -1 +1,2 @@
|
||||
export { getTableColumns } from './getTableColumns';
|
||||
export { getFKRelationships } from './getFKRelationships';
|
||||
|
@ -17,5 +17,6 @@ export const gdc: Database = {
|
||||
getTableColumns: async () => {
|
||||
return Feature.NotImplemented;
|
||||
},
|
||||
getFKRelationships: async () => Feature.NotImplemented,
|
||||
},
|
||||
};
|
||||
|
178
console/src/features/DataSource/guards.ts
Normal file
178
console/src/features/DataSource/guards.ts
Normal file
@ -0,0 +1,178 @@
|
||||
import {
|
||||
AllowedTableRelationships,
|
||||
LegacyRemoteSchemaRelationship,
|
||||
LocalTableArrayRelationship,
|
||||
LocalTableObjectRelationship,
|
||||
ManualArrayRelationship,
|
||||
ManualObjectRelationship,
|
||||
RemoteDBRelationship,
|
||||
RemoteSchemaRelationship,
|
||||
SameTableObjectRelationship,
|
||||
} from './types';
|
||||
|
||||
function isArrayOfStrings(value: any): value is string[] {
|
||||
return Array.isArray(value) && value.every(item => typeof item === 'string');
|
||||
}
|
||||
|
||||
export const isRemoteDBRelationship = (
|
||||
relationship: AllowedTableRelationships
|
||||
): relationship is RemoteDBRelationship => {
|
||||
if (!('definition' in relationship)) return false;
|
||||
|
||||
const definition = relationship.definition;
|
||||
|
||||
if (!('to_source' in definition)) return false;
|
||||
|
||||
// turn off "obj is declared but never used."
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const remoteDBRelationshipDefinition: RemoteDBRelationship['definition'] =
|
||||
definition;
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export const isRemoteSchemaRelationship = (
|
||||
relationship: AllowedTableRelationships
|
||||
): relationship is RemoteSchemaRelationship => {
|
||||
if (!('definition' in relationship)) return false;
|
||||
|
||||
const definition = relationship.definition;
|
||||
|
||||
if (!('to_remote_schema' in definition)) return false;
|
||||
|
||||
// turn off "obj is declared but never used."
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const remoteSchemarelationshipDefinition: RemoteSchemaRelationship['definition'] =
|
||||
definition;
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export const isLegacyRemoteSchemaRelationship = (
|
||||
relationship: AllowedTableRelationships
|
||||
): relationship is LegacyRemoteSchemaRelationship => {
|
||||
return (
|
||||
'definition' in relationship && 'hasura_fields' in relationship.definition
|
||||
);
|
||||
};
|
||||
|
||||
export const isManualObjectRelationship = (
|
||||
relationship: AllowedTableRelationships
|
||||
): relationship is ManualObjectRelationship => {
|
||||
if (!('using' in relationship)) return false;
|
||||
|
||||
const constraint = relationship.using;
|
||||
|
||||
if (!('manual_configuration' in constraint)) return false;
|
||||
|
||||
// turn off "obj is declared but never used."
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const objectRelationshipManualConfig: ManualObjectRelationship['using'] =
|
||||
constraint;
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export const isSameTableObjectRelationship = (
|
||||
relationship: AllowedTableRelationships
|
||||
): relationship is SameTableObjectRelationship => {
|
||||
if (!('using' in relationship)) return false;
|
||||
|
||||
const constraint = relationship.using;
|
||||
|
||||
if (!('foreign_key_constraint_on' in constraint)) return false;
|
||||
|
||||
const foreign_key_constraint_on = constraint.foreign_key_constraint_on;
|
||||
|
||||
if (
|
||||
!(
|
||||
typeof foreign_key_constraint_on === 'string' ||
|
||||
isArrayOfStrings(foreign_key_constraint_on)
|
||||
)
|
||||
)
|
||||
return false;
|
||||
|
||||
// turn off "obj is declared but never used."
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const sameTableObjectRelationshipConstraint: SameTableObjectRelationship['using']['foreign_key_constraint_on'] =
|
||||
foreign_key_constraint_on;
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export const isLocalTableObjectRelationship = (
|
||||
relationship: AllowedTableRelationships
|
||||
): relationship is LocalTableObjectRelationship => {
|
||||
if (!('using' in relationship)) return false;
|
||||
|
||||
const constraint = relationship.using;
|
||||
|
||||
if (!('foreign_key_constraint_on' in constraint)) return false;
|
||||
|
||||
const foreign_key_constraint_on = constraint.foreign_key_constraint_on;
|
||||
|
||||
if (
|
||||
typeof foreign_key_constraint_on === 'string' ||
|
||||
isArrayOfStrings(foreign_key_constraint_on)
|
||||
)
|
||||
return false;
|
||||
|
||||
// turn off "obj is declared but never used."
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const localTableObjectRelationshipConfig: LocalTableObjectRelationship['using']['foreign_key_constraint_on'] =
|
||||
foreign_key_constraint_on;
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export const isManualArrayRelationship = (
|
||||
relationship: AllowedTableRelationships
|
||||
): relationship is ManualArrayRelationship => {
|
||||
if (!('using' in relationship)) return false;
|
||||
|
||||
const constraint = relationship.using;
|
||||
|
||||
if (!('manual_configuration' in constraint)) return false;
|
||||
|
||||
// turn off "obj is declared but never used."
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const manualArrayRelationshipsConfig: ManualArrayRelationship['using'] =
|
||||
constraint;
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export const isLocalTableArrayRelationship = (
|
||||
relationship: AllowedTableRelationships
|
||||
): relationship is LocalTableArrayRelationship => {
|
||||
if (!('using' in relationship)) return false;
|
||||
|
||||
const constraint = relationship.using;
|
||||
|
||||
if (!('foreign_key_constraint_on' in constraint)) return false;
|
||||
|
||||
const foreign_key_constraint_on = constraint.foreign_key_constraint_on;
|
||||
|
||||
if (
|
||||
typeof foreign_key_constraint_on === 'string' ||
|
||||
isArrayOfStrings(foreign_key_constraint_on)
|
||||
)
|
||||
return false;
|
||||
|
||||
if (!('table' in foreign_key_constraint_on)) return false;
|
||||
|
||||
// turn off "obj is declared but never used."
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const localArrayRelationshipConfig: LocalTableArrayRelationship['using']['foreign_key_constraint_on'] =
|
||||
foreign_key_constraint_on;
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
type LegacyFkConstraint = { column: string };
|
||||
|
||||
export const isLegacyFkConstraint = (
|
||||
fkConstraint: { columns: string[] } | { column: string }
|
||||
): fkConstraint is LegacyFkConstraint => {
|
||||
return 'column' in fkConstraint;
|
||||
};
|
@ -8,15 +8,14 @@ import { gdc } from './gdc';
|
||||
import * as utils from './common/utils';
|
||||
import type {
|
||||
Property,
|
||||
Ref,
|
||||
OneOf,
|
||||
IntrospectedTable,
|
||||
MetadataTable,
|
||||
Table,
|
||||
SupportedDrivers,
|
||||
TableColumn,
|
||||
GetTrackableTablesProps,
|
||||
GetTableColumnsProps,
|
||||
TableFkRelationships,
|
||||
GetFKRelationshipProps,
|
||||
} from './types';
|
||||
|
||||
import { createZodSchema } from './common/createZodSchema';
|
||||
@ -45,6 +44,9 @@ export type Database = {
|
||||
getTableColumns: (
|
||||
props: GetTableColumnsProps
|
||||
) => Promise<TableColumn[] | Feature.NotImplemented>;
|
||||
getFKRelationships: (
|
||||
props: GetFKRelationshipProps
|
||||
) => Promise<TableFkRelationships[] | Feature.NotImplemented>;
|
||||
};
|
||||
query?: {
|
||||
getTableData: () => void;
|
||||
@ -204,18 +206,34 @@ export const DataSource = (httpClient: AxiosInstance) => ({
|
||||
});
|
||||
if (result === Feature.NotImplemented) return [];
|
||||
|
||||
return result;
|
||||
},
|
||||
getTableFkRelationships: async ({
|
||||
dataSourceName,
|
||||
table,
|
||||
}: {
|
||||
dataSourceName: string;
|
||||
table: Table;
|
||||
}) => {
|
||||
const database = await getDatabaseMethods({ dataSourceName, httpClient });
|
||||
if (!database) return [];
|
||||
|
||||
const introspection = database.introspection;
|
||||
if (!introspection) return [];
|
||||
|
||||
const result = await introspection.getFKRelationships({
|
||||
dataSourceName,
|
||||
table,
|
||||
httpClient,
|
||||
});
|
||||
|
||||
if (result === Feature.NotImplemented) return [];
|
||||
|
||||
return result;
|
||||
},
|
||||
});
|
||||
|
||||
export { exportMetadata, utils };
|
||||
export type {
|
||||
Property,
|
||||
Ref,
|
||||
OneOf,
|
||||
SupportedDrivers,
|
||||
IntrospectedTable,
|
||||
Table,
|
||||
MetadataTable,
|
||||
NetworkArgs,
|
||||
};
|
||||
|
||||
export * from './types';
|
||||
export * from './guards';
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Database, Feature } from '..';
|
||||
import { NetworkArgs, runSQL } from '../api';
|
||||
import { adaptIntrospectedTables } from '../common/utils';
|
||||
import { getTableColumns } from './introspection';
|
||||
import { getTableColumns, getFKRelationships } from './introspection';
|
||||
|
||||
export type MssqlTable = { schema: string; name: string };
|
||||
|
||||
@ -37,5 +37,6 @@ export const mssql: Database = {
|
||||
return ['schema', 'name'];
|
||||
},
|
||||
getTableColumns,
|
||||
getFKRelationships,
|
||||
},
|
||||
};
|
||||
|
@ -0,0 +1,70 @@
|
||||
import { MssqlTable } from '..';
|
||||
import { runSQL, RunSQLResponse } from '../../api';
|
||||
import { GetFKRelationshipProps } from '../../types';
|
||||
|
||||
const adaptFkRelationships = (result: RunSQLResponse['result']) => {
|
||||
if (!result) return [];
|
||||
|
||||
return result.slice(1).map(row => ({
|
||||
from: {
|
||||
table: row[0],
|
||||
column: JSON.parse(row[2]).map(
|
||||
(col: { column: string; referenced_column: string }) => col.column
|
||||
),
|
||||
},
|
||||
to: {
|
||||
table: row[1],
|
||||
column: JSON.parse(row[2]).map(
|
||||
(col: { column: string; referenced_column: string }) =>
|
||||
col.referenced_column
|
||||
),
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
export const getFKRelationships = async ({
|
||||
dataSourceName,
|
||||
table,
|
||||
httpClient,
|
||||
}: GetFKRelationshipProps) => {
|
||||
const { name } = table as MssqlTable;
|
||||
|
||||
const sql = `SELECT
|
||||
tab1.name AS [table_name],
|
||||
tab2.name AS [ref_table],
|
||||
(
|
||||
SELECT
|
||||
col1.name AS [column],
|
||||
col2.name AS [referenced_column]
|
||||
FROM sys.foreign_key_columns fkc
|
||||
INNER JOIN sys.columns col1
|
||||
ON col1.column_id = fkc.parent_column_id AND col1.object_id = tab1.object_id
|
||||
INNER JOIN sys.columns col2
|
||||
ON col2.column_id = fkc.referenced_column_id AND col2.object_id = tab2.object_id
|
||||
WHERE fk.object_id = fkc.constraint_object_id
|
||||
FOR JSON PATH
|
||||
) AS column_mapping
|
||||
FROM sys.foreign_keys fk
|
||||
INNER JOIN sys.objects obj
|
||||
ON obj.object_id = fk.referenced_object_id
|
||||
INNER JOIN sys.tables tab1
|
||||
ON tab1.object_id = fk.parent_object_id
|
||||
INNER JOIN sys.schemas sch1
|
||||
ON tab1.schema_id = sch1.schema_id
|
||||
INNER JOIN sys.tables tab2
|
||||
ON tab2.object_id = fk.referenced_object_id
|
||||
INNER JOIN sys.schemas sch2
|
||||
ON tab2.schema_id = sch2.schema_id
|
||||
WHERE tab1.name = '${name}' OR tab2.name = '${name}'`;
|
||||
|
||||
const response = await runSQL({
|
||||
source: {
|
||||
name: dataSourceName,
|
||||
kind: 'mssql',
|
||||
},
|
||||
sql,
|
||||
httpClient,
|
||||
});
|
||||
|
||||
return adaptFkRelationships(response.result);
|
||||
};
|
@ -1 +1,2 @@
|
||||
export { getTableColumns } from './getTableColumns';
|
||||
export { getFKRelationships } from './getFKRelationships';
|
||||
|
@ -3,6 +3,7 @@ import {
|
||||
getDatabaseConfiguration,
|
||||
getTrackableTables,
|
||||
getTableColumns,
|
||||
getFKRelationships,
|
||||
} from './introspection';
|
||||
|
||||
export type PostgresTable = { name: string; schema: string };
|
||||
@ -15,5 +16,6 @@ export const postgres: Database = {
|
||||
return ['schema', 'name'];
|
||||
},
|
||||
getTableColumns,
|
||||
getFKRelationships,
|
||||
},
|
||||
};
|
||||
|
@ -0,0 +1,64 @@
|
||||
import { PostgresTable } from '..';
|
||||
import { runSQL, RunSQLResponse } from '../../api';
|
||||
import { GetFKRelationshipProps } from '../../types';
|
||||
|
||||
const adaptFkRelationships = (
|
||||
result: RunSQLResponse['result'],
|
||||
schema: string
|
||||
) => {
|
||||
if (!result) return [];
|
||||
|
||||
return result.slice(1).map(row => ({
|
||||
from: {
|
||||
/**
|
||||
* This is to remove the schema name from tables that are not from `public` schema
|
||||
*/
|
||||
table: row[0]?.replace(/"/g, '').replace(`${schema}.`, ''),
|
||||
/**
|
||||
* break complex fk joins into array of string and remove and `"` character in the names
|
||||
*/
|
||||
column: row[1].split(',')?.map(i => i?.replace(/"/g, '')),
|
||||
},
|
||||
to: {
|
||||
table: row[2]?.replace(/"/g, '').replace(`${schema}.`, ''),
|
||||
column: row[3].split(',')?.map(i => i?.replace(/"/g, '')),
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
export const getFKRelationships = async ({
|
||||
dataSourceName,
|
||||
table,
|
||||
httpClient,
|
||||
}: GetFKRelationshipProps) => {
|
||||
const { schema, name } = table as PostgresTable;
|
||||
/**
|
||||
* This SQL goes through the pg_constraint (https://www.postgresql.org/docs/current/catalog-pg-constraint.html) and the pg_namespace (https://www.postgresql.org/docs/14/catalog-pg-namespace.html)
|
||||
* and links fk_constraint object that have the same Object ID on both tables and finally
|
||||
* delivers the list of all possible FKConstraints for a given table.
|
||||
* The contype checks basically checks for - f = foreign key constraint, p = primary key constraint,
|
||||
*/
|
||||
const sql = `SELECT conrelid::regclass AS "source_table"
|
||||
,CASE WHEN pg_get_constraintdef(c.oid) LIKE 'FOREIGN KEY %' THEN substring(pg_get_constraintdef(c.oid), 14, position(')' in pg_get_constraintdef(c.oid))-14) END AS "source_column"
|
||||
,CASE WHEN pg_get_constraintdef(c.oid) LIKE 'FOREIGN KEY %' THEN substring(pg_get_constraintdef(c.oid), position(' REFERENCES ' in pg_get_constraintdef(c.oid))+12, position('(' in substring(pg_get_constraintdef(c.oid), 14))-position(' REFERENCES ' in pg_get_constraintdef(c.oid))+1) END AS "target_table"
|
||||
,CASE WHEN pg_get_constraintdef(c.oid) LIKE 'FOREIGN KEY %' THEN substring(pg_get_constraintdef(c.oid), position('(' in substring(pg_get_constraintdef(c.oid), 14))+14, position(')' in substring(pg_get_constraintdef(c.oid), position('(' in substring(pg_get_constraintdef(c.oid), 14))+14))-1) END AS "target_column"
|
||||
FROM pg_constraint c
|
||||
JOIN pg_namespace n ON n.oid = c.connamespace
|
||||
WHERE contype IN ('f', 'p')
|
||||
AND (conrelid::regclass = '"${schema}"."${name}"'::regclass OR substring(pg_get_constraintdef(c.oid), position(' REFERENCES ' in pg_get_constraintdef(c.oid))+12, position('(' in substring(pg_get_constraintdef(c.oid), 14))-position(' REFERENCES ' in pg_get_constraintdef(c.oid))+1) = '${
|
||||
schema === 'public' ? `"${name}"` : `${schema}.${name}`
|
||||
}')
|
||||
AND pg_get_constraintdef(c.oid) LIKE 'FOREIGN KEY %';
|
||||
`;
|
||||
|
||||
const response = await runSQL({
|
||||
source: {
|
||||
name: dataSourceName,
|
||||
kind: 'postgres',
|
||||
},
|
||||
sql,
|
||||
httpClient,
|
||||
});
|
||||
|
||||
return adaptFkRelationships(response.result, schema);
|
||||
};
|
@ -1,3 +1,4 @@
|
||||
export { getTrackableTables } from './getTrackableTables';
|
||||
export { getDatabaseConfiguration } from './getDatabaseConfiguration';
|
||||
export { getTableColumns } from './getTableColumns';
|
||||
export { getFKRelationships } from './getFKRelationships';
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { NetworkArgs } from './api';
|
||||
import { RemoteField } from '../RemoteRelationships';
|
||||
|
||||
export type Ref = { $ref: string };
|
||||
|
||||
@ -32,9 +33,132 @@ export type SupportedDrivers =
|
||||
| 'citus'
|
||||
| 'gdc';
|
||||
|
||||
export type RemoteDBRelationship = {
|
||||
name: string;
|
||||
definition: {
|
||||
to_source: {
|
||||
relationship_type: 'object' | 'array';
|
||||
field_mapping: Record<string, string>;
|
||||
source: string;
|
||||
table: Table;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export type RemoteSchemaRelationship = {
|
||||
name: string;
|
||||
definition: {
|
||||
to_remote_schema: {
|
||||
remote_field: RemoteField;
|
||||
lhs_fields: string[];
|
||||
remote_schema: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export type LegacyRemoteSchemaRelationship = {
|
||||
name: string;
|
||||
definition: {
|
||||
hasura_fields: string[];
|
||||
remote_field: RemoteField;
|
||||
remote_schema: string;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Type based on doc - https://hasura.io/docs/latest/api-reference/metadata-api/relationship/#metadata-pg-create-object-relationship
|
||||
*/
|
||||
type BaseLocalRelationshipProps = {
|
||||
name: string;
|
||||
comment?: string;
|
||||
};
|
||||
|
||||
export type SameTableObjectRelationship = BaseLocalRelationshipProps & {
|
||||
using: {
|
||||
/**
|
||||
* Using foreign key constraint on same table, the string[] is basically the mappable column(s)
|
||||
*/
|
||||
foreign_key_constraint_on: string[] | string;
|
||||
};
|
||||
};
|
||||
|
||||
export type LocalTableObjectRelationship = BaseLocalRelationshipProps & {
|
||||
using: {
|
||||
/**
|
||||
* Using foreign key constraint on another table (within the same DB)
|
||||
*/
|
||||
foreign_key_constraint_on: {
|
||||
table: Table;
|
||||
} & (
|
||||
| {
|
||||
// Recommened type to use
|
||||
columns: string[];
|
||||
}
|
||||
| {
|
||||
// Legacy type < v2.0.10
|
||||
column: string;
|
||||
}
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
export type ManualObjectRelationship = BaseLocalRelationshipProps & {
|
||||
using: {
|
||||
/**
|
||||
* Manually create a relationship when FK relationships are not available. For eg. BigQuery does not have the concept of FKs, creating
|
||||
* relationships manually is the only way to "relate" data points
|
||||
*/
|
||||
manual_configuration: {
|
||||
remote_table: Table;
|
||||
column_mapping: Record<string, string>;
|
||||
insertion_order?: 'before_parent' | 'after_parent' | null;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export type LocalTableArrayRelationship = BaseLocalRelationshipProps & {
|
||||
using: {
|
||||
/**
|
||||
* Using foreign key constraint on another table (within the same DB)
|
||||
*/
|
||||
foreign_key_constraint_on: {
|
||||
table: Table;
|
||||
} & (
|
||||
| {
|
||||
// Recommened type to use
|
||||
columns: string[];
|
||||
}
|
||||
| {
|
||||
// Legacy type < v2.0.10
|
||||
column: string;
|
||||
}
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
export type ManualArrayRelationship = BaseLocalRelationshipProps & {
|
||||
using: {
|
||||
/**
|
||||
* Manually create a relationship when FK relationships are not available. For eg. BigQuery does not have the concept of FKs, creating
|
||||
* relationships manually is the only way to "relate" data points
|
||||
*/
|
||||
manual_configuration: {
|
||||
remote_table: Table;
|
||||
column_mapping: Record<string, string>;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
// This is the new Metadata type that we need to keep updating
|
||||
export type MetadataTable = {
|
||||
table: Record<string, string>;
|
||||
/**
|
||||
* Table definition
|
||||
*/
|
||||
table: Table;
|
||||
|
||||
/**
|
||||
* Table configuration
|
||||
*/
|
||||
configuration?: {
|
||||
custom_root_fields?: {
|
||||
select?: string;
|
||||
@ -50,8 +174,36 @@ export type MetadataTable = {
|
||||
column_config?: Record<string, { custom_name: string; comment: string }>;
|
||||
comment?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Table relationships
|
||||
*/
|
||||
remote_relationships?: (
|
||||
| RemoteDBRelationship
|
||||
| RemoteSchemaRelationship
|
||||
| LegacyRemoteSchemaRelationship
|
||||
)[];
|
||||
object_relationships?: (
|
||||
| ManualObjectRelationship
|
||||
| LocalTableObjectRelationship
|
||||
| SameTableObjectRelationship
|
||||
)[];
|
||||
array_relationships?: (
|
||||
| ManualArrayRelationship
|
||||
| LocalTableArrayRelationship
|
||||
)[];
|
||||
};
|
||||
|
||||
export type AllowedTableRelationships =
|
||||
| RemoteDBRelationship
|
||||
| RemoteSchemaRelationship
|
||||
| LegacyRemoteSchemaRelationship
|
||||
| ManualObjectRelationship
|
||||
| LocalTableObjectRelationship
|
||||
| SameTableObjectRelationship
|
||||
| ManualArrayRelationship
|
||||
| LocalTableArrayRelationship;
|
||||
|
||||
export type Source = {
|
||||
name: string;
|
||||
kind: SupportedDrivers;
|
||||
@ -100,3 +252,20 @@ export type GetTableColumnsProps = {
|
||||
dataSourceName: string;
|
||||
table: Table;
|
||||
} & NetworkArgs;
|
||||
export type GetFKRelationshipProps = {
|
||||
dataSourceName: string;
|
||||
table: Table;
|
||||
} & NetworkArgs;
|
||||
|
||||
export type TableFkRelationships = {
|
||||
from: {
|
||||
table: string;
|
||||
column: string[];
|
||||
};
|
||||
to: {
|
||||
table: string;
|
||||
column: string[];
|
||||
};
|
||||
};
|
||||
|
||||
export { NetworkArgs };
|
||||
|
@ -3,10 +3,51 @@ import {
|
||||
useTableRelationships,
|
||||
TableRelationshipsType,
|
||||
} from '@/features/Datasources';
|
||||
import { RowData } from '@/features/RelationshipsTable';
|
||||
import { ArrayRelationship, ObjectRelationship } from '@/metadata/types';
|
||||
import { DbToDbRelationship, DbToRemoteSchemaRelationship } from '../types';
|
||||
import { useArrayRelationships } from './useArrayRelationships';
|
||||
import { useObjectRelationships } from './useObjectRelationships';
|
||||
|
||||
type BaseTypes = {
|
||||
name: string;
|
||||
reference: string;
|
||||
referenceTable: string;
|
||||
target: string;
|
||||
targetTable?: string;
|
||||
fieldsFrom: string[];
|
||||
fieldsTo: string[];
|
||||
};
|
||||
|
||||
type DbToDb = {
|
||||
fromType: 'table';
|
||||
toType: 'database';
|
||||
type: 'Object' | 'Array';
|
||||
relationship: DbToDbRelationship;
|
||||
} & BaseTypes;
|
||||
|
||||
type Remote = {
|
||||
fromType: 'table';
|
||||
toType: 'remote_schema';
|
||||
type: 'Remote Schema';
|
||||
relationship: DbToRemoteSchemaRelationship;
|
||||
};
|
||||
|
||||
type LocalArray = {
|
||||
fromType: 'table';
|
||||
toType: 'table';
|
||||
type: 'Array';
|
||||
relationship: ArrayRelationship;
|
||||
};
|
||||
|
||||
type LocalObject = {
|
||||
fromType: 'table';
|
||||
toType: 'table';
|
||||
type: 'Object';
|
||||
relationship: ObjectRelationship;
|
||||
};
|
||||
|
||||
type RowData = (DbToDb | LocalObject | LocalArray | Remote) & BaseTypes;
|
||||
|
||||
const getRelationshipMap = (
|
||||
tableRlns: TableRelationshipsType[],
|
||||
fkConstraint?: string
|
||||
|
@ -1,11 +1,10 @@
|
||||
import { baseUrl as baseURL } from '@/Endpoints';
|
||||
import { ReduxState } from '@/types';
|
||||
import axios from 'axios';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
export const useHttpClient = ({
|
||||
headers,
|
||||
}: {
|
||||
headers: Record<string, string>;
|
||||
}) => {
|
||||
export const useHttpClient = () => {
|
||||
const headers = useSelector((state: ReduxState) => state.tables.dataHeaders);
|
||||
return axios.create({
|
||||
baseURL,
|
||||
headers,
|
||||
|
@ -13,7 +13,7 @@ import { metadata, relationshipQueryResponse } from './mocks';
|
||||
const url = 'http://localhost:8080';
|
||||
|
||||
export default {
|
||||
title: 'Features/Relationships/Database Relationship Table',
|
||||
title: 'Data/Relationships/View Relationships',
|
||||
component: DatabaseRelationshipsTable,
|
||||
decorators: [ReactQueryDecorator()],
|
||||
parameters: {
|
||||
@ -33,5 +33,6 @@ export const Primary: Story<DatabaseRelationshipsTableProps> = args => (
|
||||
);
|
||||
|
||||
Primary.args = {
|
||||
target: { database: 'default', table: 'Album', schema: 'public' },
|
||||
dataSourceName: 'chinook',
|
||||
table: { name: 'Employee', schema: 'public' },
|
||||
};
|
||||
|
@ -1,114 +1,126 @@
|
||||
import React from 'react';
|
||||
|
||||
import { FaArrowRight } from 'react-icons/fa';
|
||||
// eslint-disable-next-line import/first
|
||||
import { Table } from '@/features/DataSource';
|
||||
import {
|
||||
FaArrowRight,
|
||||
FaColumns,
|
||||
FaDatabase,
|
||||
FaEdit,
|
||||
FaFont,
|
||||
FaPlug,
|
||||
FaTable,
|
||||
FaTrash,
|
||||
} from 'react-icons/fa';
|
||||
import { CardedTable } from '@/new-components/CardedTable';
|
||||
import {
|
||||
useDbToRemoteSchemaRelationships,
|
||||
useDbToRemoteDbRelationships,
|
||||
useLocalRelationships,
|
||||
} from '@/features/MetadataAPI';
|
||||
import { DataTarget } from '@/features/Datasources';
|
||||
import { IndicatorCard } from '@/new-components/IndicatorCard';
|
||||
import {
|
||||
ModifyActions,
|
||||
TableRowIcon,
|
||||
SourceRelationshipCell,
|
||||
DestinationRelationshipCell,
|
||||
} from './components';
|
||||
|
||||
import { getRemoteFieldPath } from '../utils';
|
||||
import { RowData } from './types';
|
||||
import { Relationship } from './types';
|
||||
import Legends from './Legends';
|
||||
import { useListAllRelationshipsFromMetadata } from './hooks/useListAllRelationshipsFromMetadata';
|
||||
|
||||
export const columns = ['NAME', 'SOURCE', 'TYPE', 'RELATIONSHIP', null];
|
||||
|
||||
// fetch the data from the relevant hooks
|
||||
const useTableData = (target: DataTarget) => {
|
||||
const {
|
||||
data: dbToRsData,
|
||||
isLoading: dbToRsIsLoading,
|
||||
isError: dbToRsIsError,
|
||||
} = useDbToRemoteSchemaRelationships(target);
|
||||
const getTableDisplayName = (table: Table): string => {
|
||||
if (!table) return 'Empty Object';
|
||||
|
||||
const { data: localrelationships, isLoading: localRelnsIsLoading } =
|
||||
useLocalRelationships(target);
|
||||
if (typeof table === 'string') return table;
|
||||
|
||||
const {
|
||||
data: dbToDbData,
|
||||
isLoading: dbToDbIsLoading,
|
||||
isError: dbToDbIsError,
|
||||
} = useDbToRemoteDbRelationships(target);
|
||||
if (typeof table === 'object' && 'name' in table)
|
||||
return (table as { name: string }).name;
|
||||
|
||||
// convert it into a consistent useful format for the table
|
||||
const data = React.useMemo(() => {
|
||||
// other relationships will be added here
|
||||
const dbToRs: RowData[] =
|
||||
dbToRsData?.map(relationship => ({
|
||||
fromType: 'table',
|
||||
toType: 'remote_schema',
|
||||
name: relationship?.relationshipName,
|
||||
reference: target.database,
|
||||
referenceTable: target.table,
|
||||
target: relationship.remoteSchemaName,
|
||||
type: 'Remote Schema',
|
||||
fieldsFrom: relationship?.lhs_fields || [],
|
||||
fieldsTo: getRemoteFieldPath(relationship?.remote_field),
|
||||
return JSON.stringify(table);
|
||||
};
|
||||
|
||||
function TargetName(props: { relationship: Relationship }) {
|
||||
const { relationship } = props;
|
||||
if (relationship.type === 'toRemoteSchema')
|
||||
return (
|
||||
<>
|
||||
<FaPlug /> <span>{relationship.mapping.to.remoteSchema}</span>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<FaDatabase /> <span>{relationship.mapping.to.source}</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const RelationshipMapping = ({
|
||||
relationship,
|
||||
})) || [];
|
||||
}: {
|
||||
relationship: Relationship;
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<FaTable />
|
||||
<span>{getTableDisplayName(relationship.mapping.from.table)}</span>
|
||||
/
|
||||
<FaColumns /> {relationship.mapping.from.columns.join(',')}
|
||||
</div>
|
||||
<FaArrowRight />
|
||||
|
||||
const dbToDb: RowData[] =
|
||||
dbToDbData?.map(relationship => ({
|
||||
fromType: 'table',
|
||||
toType: 'database',
|
||||
name: relationship?.relationshipName,
|
||||
reference: target.database,
|
||||
referenceTable: target.table,
|
||||
target: relationship.remoteDbName,
|
||||
...(relationship?.target?.table && {
|
||||
targetTable: relationship?.target?.table,
|
||||
}),
|
||||
type: relationship.relationshipType === 'array' ? 'Array' : 'Object',
|
||||
fieldsFrom: Object.keys(relationship.fieldMapping) || [],
|
||||
fieldsTo: Object.values(relationship.fieldMapping) || [],
|
||||
relationship,
|
||||
})) || [];
|
||||
|
||||
return [...localrelationships, ...dbToRs, ...dbToDb];
|
||||
}, [dbToRsData, dbToDbData, localrelationships, target]);
|
||||
|
||||
const isLoading = dbToRsIsLoading || dbToDbIsLoading || localRelnsIsLoading;
|
||||
const isError = dbToRsIsError || dbToDbIsError;
|
||||
|
||||
return { data, isLoading, isError };
|
||||
<div className="flex items-center gap-2">
|
||||
{relationship.type === 'toRemoteSchema' ? (
|
||||
<>
|
||||
<FaPlug />
|
||||
<span>{relationship.mapping.to.remoteSchema}</span> /
|
||||
<FaFont /> {relationship.mapping.to.fields.join(',')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<FaTable />
|
||||
<span>{getTableDisplayName(relationship.mapping.to.table)}</span> /
|
||||
<FaColumns />
|
||||
{!relationship.mapping.to.columns.length
|
||||
? '[could not find target columns]'
|
||||
: relationship.mapping.to.columns.join(',')}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export interface DatabaseRelationshipsTableProps {
|
||||
target: DataTarget;
|
||||
onClick: (input: OnClickHandlerArgs) => void;
|
||||
}
|
||||
|
||||
export interface OnClickHandlerArgs {
|
||||
type: 'add' | 'edit' | 'delete' | 'close';
|
||||
row?: RowData;
|
||||
dataSourceName: string;
|
||||
table: Table;
|
||||
onEditRow: (props: {
|
||||
dataSourceName: string;
|
||||
table: Table;
|
||||
relationship: Relationship;
|
||||
}) => void;
|
||||
onDeleteRow: (props: {
|
||||
dataSourceName: string;
|
||||
table: Table;
|
||||
relationship: Relationship;
|
||||
}) => void;
|
||||
}
|
||||
|
||||
export const DatabaseRelationshipsTable = ({
|
||||
target,
|
||||
onClick,
|
||||
dataSourceName,
|
||||
table,
|
||||
onEditRow,
|
||||
onDeleteRow,
|
||||
}: DatabaseRelationshipsTableProps) => {
|
||||
const { data, isLoading, isError } = useTableData(target);
|
||||
const {
|
||||
data: relationships,
|
||||
isLoading,
|
||||
isError,
|
||||
} = useListAllRelationshipsFromMetadata(dataSourceName, table);
|
||||
if (isError && !isLoading)
|
||||
return (
|
||||
<IndicatorCard
|
||||
status="negative"
|
||||
headline="Error fetching relationships"
|
||||
headline="Error while fetching relationships"
|
||||
/>
|
||||
);
|
||||
|
||||
if (!data && isLoading)
|
||||
if (!relationships && isLoading)
|
||||
return <IndicatorCard status="info" headline="Fetching relationships..." />;
|
||||
|
||||
if (!data || data?.length === 0)
|
||||
if (!relationships || relationships.length === 0)
|
||||
return <IndicatorCard status="info" headline="No relationships found" />;
|
||||
|
||||
return (
|
||||
@ -116,43 +128,66 @@ export const DatabaseRelationshipsTable = ({
|
||||
<CardedTable.Table>
|
||||
<CardedTable.Header columns={columns} />
|
||||
<CardedTable.TableBody>
|
||||
{data.map(row => (
|
||||
<CardedTable.TableBodyRow key={row.name}>
|
||||
{relationships.map(relationship => (
|
||||
<CardedTable.TableBodyRow key={relationship.name}>
|
||||
<CardedTable.TableBodyCell>
|
||||
<button
|
||||
className="text-secondary cursor-pointer"
|
||||
onClick={() => onClick({ type: 'edit', row })}
|
||||
onClick={() =>
|
||||
onEditRow({
|
||||
dataSourceName,
|
||||
table,
|
||||
relationship,
|
||||
})
|
||||
}
|
||||
>
|
||||
{row.name}
|
||||
{relationship.name}
|
||||
</button>
|
||||
</CardedTable.TableBodyCell>
|
||||
|
||||
<CardedTable.TableBodyCell>
|
||||
<div className="flex items-center gap-2">
|
||||
<TableRowIcon
|
||||
type={
|
||||
row.toType === 'remote_schema'
|
||||
? 'remote_schema'
|
||||
: 'database'
|
||||
}
|
||||
/>
|
||||
<span>{row.target}</span>
|
||||
<TargetName relationship={relationship} />
|
||||
</div>
|
||||
</CardedTable.TableBodyCell>
|
||||
<CardedTable.TableBodyCell>{row.type}</CardedTable.TableBodyCell>
|
||||
|
||||
<CardedTable.TableBodyCell>
|
||||
<SourceRelationshipCell row={row} />
|
||||
{relationship.relationship_type}
|
||||
</CardedTable.TableBodyCell>
|
||||
|
||||
<CardedTable.TableBodyCell>
|
||||
<FaArrowRight className="fill-current text-sm text-muted" />
|
||||
</CardedTable.TableBodyCell>
|
||||
<CardedTable.TableBodyCell>
|
||||
<DestinationRelationshipCell row={row} />
|
||||
<RelationshipMapping relationship={relationship} />
|
||||
</CardedTable.TableBodyCell>
|
||||
|
||||
<CardedTable.TableBodyActionCell>
|
||||
<ModifyActions
|
||||
onEdit={() => onClick({ type: 'edit', row })}
|
||||
onDelete={() => onClick({ type: 'delete', row })}
|
||||
/>
|
||||
<div className="flex items-center justify-end whitespace-nowrap text-right">
|
||||
<button
|
||||
onClick={() =>
|
||||
onEditRow({
|
||||
dataSourceName,
|
||||
table,
|
||||
relationship,
|
||||
})
|
||||
}
|
||||
className="flex px-2 py-0.5 items-center font-semibold rounded text-secondary mr-0.5 hover:bg-indigo-50 focus:bg-indigo-100"
|
||||
>
|
||||
<FaEdit className="fill-current mr-1" />
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
onDeleteRow({
|
||||
dataSourceName,
|
||||
table,
|
||||
relationship,
|
||||
})
|
||||
}
|
||||
className="flex px-2 py-0.5 items-center font-semibold rounded text-red-700 hover:bg-red-50 focus:bg-red-100"
|
||||
>
|
||||
<FaTrash className="fill-current mr-1" />
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
</CardedTable.TableBodyActionCell>
|
||||
</CardedTable.TableBodyRow>
|
||||
))}
|
||||
|
@ -0,0 +1,164 @@
|
||||
import { Relationship } from '../types';
|
||||
|
||||
export const expectedRemoteSchemaRelationshipOutput: Relationship & {
|
||||
type: 'toRemoteSchema';
|
||||
} = {
|
||||
name: 'new_rs_to_db',
|
||||
type: 'toRemoteSchema',
|
||||
toRemoteSchema: 'rs',
|
||||
relationship_type: 'Remote Schema',
|
||||
mapping: {
|
||||
from: {
|
||||
source: 'chinook',
|
||||
table: {
|
||||
name: 'Employee',
|
||||
schema: 'public',
|
||||
},
|
||||
columns: ['EmployeeId'],
|
||||
},
|
||||
to: {
|
||||
remoteSchema: 'rs',
|
||||
fields: ['country'],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const expectedLegacyRemoteSchemaRelationshipsOutput: Relationship & {
|
||||
type: 'toRemoteSchema';
|
||||
} = {
|
||||
name: 'legacy_db_to_rs',
|
||||
type: 'toRemoteSchema',
|
||||
toRemoteSchema: 'rs',
|
||||
relationship_type: 'Remote Schema',
|
||||
mapping: {
|
||||
from: {
|
||||
source: 'chinook',
|
||||
table: {
|
||||
name: 'Employee',
|
||||
schema: 'public',
|
||||
},
|
||||
columns: ['EmployeeId'],
|
||||
},
|
||||
to: {
|
||||
remoteSchema: 'rs',
|
||||
fields: ['country'],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const expectedRemoteDBRelationshipOutput: Relationship & {
|
||||
type: 'toSource';
|
||||
} = {
|
||||
name: 'remote_db_object_rel',
|
||||
type: 'toSource',
|
||||
toSource: 'bikes',
|
||||
relationship_type: 'object',
|
||||
mapping: {
|
||||
from: {
|
||||
source: 'chinook',
|
||||
table: {
|
||||
name: 'Employee',
|
||||
schema: 'public',
|
||||
},
|
||||
columns: ['EmployeeId'],
|
||||
},
|
||||
to: {
|
||||
source: 'bikes',
|
||||
table: {
|
||||
name: 'brands',
|
||||
schema: 'production',
|
||||
},
|
||||
columns: ['brand_id'],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const expectedManualLocalRelationshipOutput: Relationship & {
|
||||
type: 'toLocalTableManual';
|
||||
} = {
|
||||
name: 'local_array_rel',
|
||||
type: 'toLocalTableManual',
|
||||
toLocalTable: {
|
||||
name: 'Album',
|
||||
schema: 'public',
|
||||
},
|
||||
relationship_type: 'Object',
|
||||
mapping: {
|
||||
from: {
|
||||
source: 'chinook',
|
||||
table: {
|
||||
name: 'Employee',
|
||||
schema: 'public',
|
||||
},
|
||||
columns: ['EmployeeId'],
|
||||
},
|
||||
to: {
|
||||
source: 'chinook',
|
||||
table: {
|
||||
name: 'Album',
|
||||
schema: 'public',
|
||||
},
|
||||
columns: ['AlbumId'],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const expectedLocalTableRelationships: Relationship & {
|
||||
type: 'toLocalTableFk';
|
||||
} = {
|
||||
name: 'Employees',
|
||||
type: 'toLocalTableFk',
|
||||
toLocalTable: {
|
||||
name: 'Employee',
|
||||
schema: 'public',
|
||||
},
|
||||
relationship_type: 'Object',
|
||||
mapping: {
|
||||
from: {
|
||||
source: 'chinook',
|
||||
table: {
|
||||
name: 'Employee',
|
||||
schema: 'public',
|
||||
},
|
||||
columns: ['ReportsTo'],
|
||||
},
|
||||
to: {
|
||||
source: 'chinook',
|
||||
table: {
|
||||
name: 'Employee',
|
||||
schema: 'public',
|
||||
},
|
||||
columns: ['EmployeeId'],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const expectedSameTableObjectRelationships: Relationship & {
|
||||
type: 'toSameTableFk';
|
||||
} = {
|
||||
name: 'Employee',
|
||||
type: 'toSameTableFk',
|
||||
toLocalTable: {
|
||||
name: 'Employee',
|
||||
schema: 'public',
|
||||
},
|
||||
relationship_type: 'Object',
|
||||
mapping: {
|
||||
from: {
|
||||
source: 'chinook',
|
||||
table: {
|
||||
name: 'Employee',
|
||||
schema: 'public',
|
||||
},
|
||||
columns: ['ReportsTo'],
|
||||
},
|
||||
to: {
|
||||
source: 'chinook',
|
||||
table: {
|
||||
name: 'Employee',
|
||||
schema: 'public',
|
||||
},
|
||||
columns: ['EmployeeId'],
|
||||
},
|
||||
},
|
||||
};
|
@ -0,0 +1,190 @@
|
||||
import {
|
||||
expectedRemoteSchemaRelationshipOutput,
|
||||
expectedLegacyRemoteSchemaRelationshipsOutput,
|
||||
expectedRemoteDBRelationshipOutput,
|
||||
expectedManualLocalRelationshipOutput,
|
||||
expectedLocalTableRelationships,
|
||||
expectedSameTableObjectRelationships,
|
||||
} from './mocks';
|
||||
import {
|
||||
adaptRemoteSchemaRelationship,
|
||||
adaptLegacyRemoteSchemaRelationship,
|
||||
adaptRemoteDBRelationship,
|
||||
adaptManualRelationship,
|
||||
adaptLocalTableRelationship,
|
||||
adaptSameTableObjectRelationship,
|
||||
} from '../hooks/useListAllRelationshipsFromMetadata/utils';
|
||||
|
||||
describe('test adapters', () => {
|
||||
it('for remote schema relationship', async () => {
|
||||
const result = adaptRemoteSchemaRelationship(
|
||||
'chinook',
|
||||
{ name: 'Employee', schema: 'public' },
|
||||
{
|
||||
definition: {
|
||||
to_remote_schema: {
|
||||
lhs_fields: ['EmployeeId'],
|
||||
remote_field: {
|
||||
country: {
|
||||
arguments: {
|
||||
code: '$EmployeeId',
|
||||
},
|
||||
},
|
||||
},
|
||||
remote_schema: 'rs',
|
||||
},
|
||||
},
|
||||
name: 'new_rs_to_db',
|
||||
}
|
||||
);
|
||||
expect(result).toEqual(expectedRemoteSchemaRelationshipOutput);
|
||||
});
|
||||
|
||||
it('for legacy remote schema relationship', async () => {
|
||||
const result = adaptLegacyRemoteSchemaRelationship(
|
||||
'chinook',
|
||||
{ name: 'Employee', schema: 'public' },
|
||||
|
||||
{
|
||||
definition: {
|
||||
hasura_fields: ['EmployeeId'],
|
||||
remote_field: {
|
||||
country: {
|
||||
arguments: {
|
||||
code: '$EmployeeId',
|
||||
},
|
||||
},
|
||||
},
|
||||
remote_schema: 'rs',
|
||||
},
|
||||
name: 'legacy_db_to_rs',
|
||||
}
|
||||
);
|
||||
expect(result).toEqual(expectedLegacyRemoteSchemaRelationshipsOutput);
|
||||
});
|
||||
|
||||
it('for remote db relationship', async () => {
|
||||
const result = adaptRemoteDBRelationship(
|
||||
'chinook',
|
||||
{ name: 'Employee', schema: 'public' },
|
||||
{
|
||||
definition: {
|
||||
to_source: {
|
||||
field_mapping: {
|
||||
EmployeeId: 'brand_id',
|
||||
},
|
||||
relationship_type: 'object',
|
||||
source: 'bikes',
|
||||
table: {
|
||||
name: 'brands',
|
||||
schema: 'production',
|
||||
},
|
||||
},
|
||||
},
|
||||
name: 'remote_db_object_rel',
|
||||
}
|
||||
);
|
||||
expect(result).toEqual(expectedRemoteDBRelationshipOutput);
|
||||
});
|
||||
|
||||
it('for local relationships created manually', async () => {
|
||||
const result = adaptManualRelationship(
|
||||
'chinook',
|
||||
{ name: 'Employee', schema: 'public' },
|
||||
{
|
||||
name: 'local_array_rel',
|
||||
using: {
|
||||
manual_configuration: {
|
||||
column_mapping: {
|
||||
EmployeeId: 'AlbumId',
|
||||
},
|
||||
insertion_order: null,
|
||||
remote_table: {
|
||||
name: 'Album',
|
||||
schema: 'public',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
expect(result).toEqual(expectedManualLocalRelationshipOutput);
|
||||
});
|
||||
|
||||
it('for local relationships created via foreign keys', async () => {
|
||||
const result = adaptLocalTableRelationship(
|
||||
'chinook',
|
||||
{ name: 'Employee', schema: 'public' },
|
||||
{
|
||||
name: 'Employees',
|
||||
using: {
|
||||
foreign_key_constraint_on: {
|
||||
column: 'ReportsTo',
|
||||
table: {
|
||||
name: 'Employee',
|
||||
schema: 'public',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
[
|
||||
{
|
||||
from: {
|
||||
table: 'Employee',
|
||||
column: ['ReportsTo'],
|
||||
},
|
||||
to: {
|
||||
table: 'Employee',
|
||||
column: ['EmployeeId'],
|
||||
},
|
||||
},
|
||||
{
|
||||
from: {
|
||||
table: 'Customer',
|
||||
column: ['SupportRepId'],
|
||||
},
|
||||
to: {
|
||||
table: 'Employee',
|
||||
column: ['EmployeeId'],
|
||||
},
|
||||
},
|
||||
]
|
||||
);
|
||||
expect(result).toEqual(expectedLocalTableRelationships);
|
||||
});
|
||||
|
||||
it('for same table relationships created via foreign keys', async () => {
|
||||
const result = adaptSameTableObjectRelationship(
|
||||
'chinook',
|
||||
{ name: 'Employee', schema: 'public' },
|
||||
{
|
||||
name: 'Employee',
|
||||
using: {
|
||||
foreign_key_constraint_on: 'ReportsTo',
|
||||
},
|
||||
},
|
||||
[
|
||||
{
|
||||
from: {
|
||||
table: 'Employee',
|
||||
column: ['ReportsTo'],
|
||||
},
|
||||
to: {
|
||||
table: 'Employee',
|
||||
column: ['EmployeeId'],
|
||||
},
|
||||
},
|
||||
{
|
||||
from: {
|
||||
table: 'Customer',
|
||||
column: ['SupportRepId'],
|
||||
},
|
||||
to: {
|
||||
table: 'Employee',
|
||||
column: ['EmployeeId'],
|
||||
},
|
||||
},
|
||||
]
|
||||
);
|
||||
expect(result).toEqual(expectedSameTableObjectRelationships);
|
||||
});
|
||||
});
|
@ -1,31 +0,0 @@
|
||||
import React from 'react';
|
||||
import { TableRowIcon } from './TableRowIcon';
|
||||
import { RowData } from '../types';
|
||||
|
||||
interface RelationshipCellProps {
|
||||
row: RowData;
|
||||
}
|
||||
|
||||
export const DestinationRelationshipCell = ({ row }: RelationshipCellProps) => {
|
||||
// these will be updated when the table also handles other db to x relationships
|
||||
const secondaryIconToType =
|
||||
row.toType === 'remote_schema' ? 'remote_schema_leaf' : 'table_leaf';
|
||||
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
<div className="flex gap-2 items-center">
|
||||
<TableRowIcon
|
||||
type={row.toType === 'remote_schema' ? 'remote_schema' : 'table'}
|
||||
/>
|
||||
{row.toType === 'remote_schema' ? row?.target : row?.targetTable}
|
||||
{row?.fieldsTo.map(field => (
|
||||
<React.Fragment key={field}>
|
||||
/
|
||||
<TableRowIcon type={secondaryIconToType} />
|
||||
{field}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,26 +0,0 @@
|
||||
import React from 'react';
|
||||
import { FaEdit, FaTrash } from 'react-icons/fa';
|
||||
|
||||
interface ModifyActionsColProps {
|
||||
onEdit: () => void;
|
||||
onDelete: () => void;
|
||||
}
|
||||
|
||||
export const ModifyActions = ({ onEdit, onDelete }: ModifyActionsColProps) => (
|
||||
<div className="flex items-center justify-end whitespace-nowrap text-right">
|
||||
<button
|
||||
onClick={onEdit}
|
||||
className="flex px-2 py-0.5 items-center font-semibold rounded text-secondary mr-0.5 hover:bg-indigo-50 focus:bg-indigo-100"
|
||||
>
|
||||
<FaEdit className="fill-current mr-1" />
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
onClick={onDelete}
|
||||
className="flex px-2 py-0.5 items-center font-semibold rounded text-red-700 hover:bg-red-50 focus:bg-red-100"
|
||||
>
|
||||
<FaTrash className="fill-current mr-1" />
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
);
|
@ -1,44 +0,0 @@
|
||||
import React from 'react';
|
||||
import { FaArrowRight } from 'react-icons/fa';
|
||||
|
||||
import { TableRowIcon } from './TableRowIcon';
|
||||
import { RowData } from '../types';
|
||||
|
||||
interface RelationshipCellProps {
|
||||
row: RowData;
|
||||
}
|
||||
|
||||
export const RelationshipCell = ({ row }: RelationshipCellProps) => {
|
||||
// these will be updated when the table also handles other db to x relationships
|
||||
const secondaryIconFromType = 'table_leaf';
|
||||
const secondaryIconToType =
|
||||
row.toType === 'remote_schema' ? 'remote_schema_leaf' : 'table_leaf';
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex gap-2 items-center">
|
||||
<TableRowIcon type={row.fromType} />
|
||||
{row?.reference}
|
||||
{row?.fieldsFrom.map(field => (
|
||||
<React.Fragment key={field}>
|
||||
/
|
||||
<TableRowIcon type={secondaryIconFromType} />
|
||||
{field}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
<FaArrowRight className="fill-current text-sm text-muted col-span-1" />
|
||||
<div className="flex gap-2 items-center">
|
||||
<TableRowIcon type={row.toType} />
|
||||
{row?.target}
|
||||
{row?.fieldsTo.map(field => (
|
||||
<React.Fragment key={field}>
|
||||
/
|
||||
<TableRowIcon type={secondaryIconToType} />
|
||||
{field}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,29 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import { TableRowIcon } from './TableRowIcon';
|
||||
import { RowData } from '../types';
|
||||
|
||||
interface RelationshipCellProps {
|
||||
row: RowData;
|
||||
}
|
||||
|
||||
export const SourceRelationshipCell = ({ row }: RelationshipCellProps) => {
|
||||
// these will be updated when the table also handles other db to x relationships
|
||||
const secondaryIconFromType = 'table_leaf';
|
||||
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
<div className="flex gap-2 items-center">
|
||||
<TableRowIcon type={row.fromType} />
|
||||
{row?.referenceTable}
|
||||
{row?.fieldsFrom.map(field => (
|
||||
<React.Fragment key={field}>
|
||||
/
|
||||
<TableRowIcon type={secondaryIconFromType} />
|
||||
{field}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,30 +0,0 @@
|
||||
import React from 'react';
|
||||
import { FaColumns, FaDatabase, FaFont, FaPlug, FaTable } from 'react-icons/fa';
|
||||
|
||||
const className = 'fill-current text-sm text-muted p-0';
|
||||
|
||||
interface TableRowIconProps {
|
||||
type:
|
||||
| 'database'
|
||||
| 'table'
|
||||
| 'remote_schema'
|
||||
| 'remote_schema_leaf'
|
||||
| 'table_leaf';
|
||||
}
|
||||
|
||||
export const TableRowIcon = ({ type }: TableRowIconProps) => {
|
||||
switch (type) {
|
||||
case 'database':
|
||||
return <FaDatabase className={className} title="Database" />;
|
||||
case 'table':
|
||||
return <FaTable className={className} title="Table" />;
|
||||
case 'remote_schema':
|
||||
return <FaPlug className={className} title="Remote Schema" />;
|
||||
case 'remote_schema_leaf':
|
||||
return <FaFont className={className} title="Field" />;
|
||||
case 'table_leaf':
|
||||
return <FaColumns className={className} title="Column" />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
@ -1,5 +0,0 @@
|
||||
export * from './ModifyActions';
|
||||
export * from './RelationshipsCell';
|
||||
export * from './SourceRelationshipsCell';
|
||||
export * from './DestinationRelationshipCell';
|
||||
export * from './TableRowIcon';
|
@ -0,0 +1,48 @@
|
||||
import { exportMetadata, Table } from '@/features/DataSource';
|
||||
import { useHttpClient } from '@/features/Network';
|
||||
import { useQuery } from 'react-query';
|
||||
import { areTablesEqual } from '../../utils';
|
||||
|
||||
export const useFindRelationship = ({
|
||||
dataSourceName,
|
||||
relationshipName,
|
||||
table,
|
||||
}: {
|
||||
dataSourceName: string;
|
||||
table: Table;
|
||||
relationshipName: string;
|
||||
}) => {
|
||||
const httpClient = useHttpClient();
|
||||
return useQuery({
|
||||
queryKey: ['get_existing_relationship', dataSourceName, relationshipName],
|
||||
queryFn: async () => {
|
||||
const metadata = await exportMetadata({ httpClient });
|
||||
const metadataSource = metadata.sources.find(
|
||||
source => source.name === dataSourceName
|
||||
);
|
||||
|
||||
if (!metadataSource) throw Error('source not found');
|
||||
|
||||
const metadataTable = metadataSource.tables.find(t =>
|
||||
areTablesEqual(table, t.table)
|
||||
);
|
||||
if (!metadataTable) throw Error('table not found');
|
||||
|
||||
/**
|
||||
* Look for the relationship inside array_relationship, object_relationships & remote_relationships
|
||||
*/
|
||||
|
||||
const allRelationships = [
|
||||
...(metadataTable.object_relationships ?? []),
|
||||
...(metadataTable.array_relationships ?? []),
|
||||
...(metadataTable.remote_relationships ?? []),
|
||||
];
|
||||
|
||||
const relationship = allRelationships.find(
|
||||
rel => rel.name === relationshipName
|
||||
);
|
||||
|
||||
return relationship;
|
||||
},
|
||||
});
|
||||
};
|
@ -0,0 +1 @@
|
||||
export { useListAllRelationshipsFromMetadata } from './useListAllRelationshipsFromMetadata';
|
@ -0,0 +1,172 @@
|
||||
import {
|
||||
exportMetadata,
|
||||
Table,
|
||||
isLocalTableObjectRelationship,
|
||||
isManualArrayRelationship,
|
||||
isManualObjectRelationship,
|
||||
isRemoteDBRelationship,
|
||||
isRemoteSchemaRelationship,
|
||||
DataSource,
|
||||
} from '@/features/DataSource';
|
||||
import { useHttpClient } from '@/features/Network';
|
||||
import { useQuery } from 'react-query';
|
||||
import { areTablesEqual } from '../../../utils';
|
||||
import { Relationship } from '../../types';
|
||||
import {
|
||||
adaptLegacyRemoteSchemaRelationship,
|
||||
adaptLocalTableRelationship,
|
||||
adaptManualRelationship,
|
||||
adaptRemoteDBRelationship,
|
||||
adaptRemoteSchemaRelationship,
|
||||
adaptSameTableObjectRelationship,
|
||||
} from './utils';
|
||||
|
||||
export const useListAllRelationshipsFromMetadata = (
|
||||
dataSourceName: string,
|
||||
table: Table
|
||||
) => {
|
||||
const httpClient = useHttpClient();
|
||||
return useQuery({
|
||||
queryKey: [dataSourceName, 'list_all_relationships'],
|
||||
refetchOnWindowFocus: false,
|
||||
queryFn: async () => {
|
||||
const metadata = await exportMetadata({ httpClient });
|
||||
const metadataSource = metadata.sources.find(
|
||||
source => source.name === dataSourceName
|
||||
);
|
||||
|
||||
const fkRelationships = await DataSource(
|
||||
httpClient
|
||||
).getTableFkRelationships({
|
||||
dataSourceName,
|
||||
table,
|
||||
});
|
||||
|
||||
/**
|
||||
* If I can't find the source in the metadata, then something is inconsistent. Panic and throw error
|
||||
*/
|
||||
if (!metadataSource)
|
||||
throw Error(
|
||||
'useListAllRelationshipsFromMetadata: unable to find a metadata.source that matches dataSourceName'
|
||||
);
|
||||
|
||||
// FIX ME: use Matt's util function for the equal-to check later
|
||||
const metadataTable = metadataSource.tables.find(t => {
|
||||
return areTablesEqual(t.table, table);
|
||||
});
|
||||
|
||||
/**
|
||||
* If I can't find the table in the metadataSource, then something is inconsistent. Panic and throw error
|
||||
*/
|
||||
if (!metadataTable)
|
||||
throw Error(
|
||||
'useListAllRelationshipsFromMetadata: unable to find a metadata.source.table that matches table'
|
||||
);
|
||||
|
||||
/**
|
||||
* Collect all relationships - both local & remote
|
||||
*/
|
||||
let relationships: Relationship[] = [];
|
||||
|
||||
/**
|
||||
* Remote relationships
|
||||
*/
|
||||
if (metadataTable.remote_relationships) {
|
||||
relationships = [
|
||||
...relationships,
|
||||
...metadataTable.remote_relationships.map<Relationship>(
|
||||
relationship => {
|
||||
if (isRemoteDBRelationship(relationship))
|
||||
return adaptRemoteDBRelationship(
|
||||
dataSourceName,
|
||||
table,
|
||||
relationship
|
||||
);
|
||||
|
||||
if (isRemoteSchemaRelationship(relationship))
|
||||
return adaptRemoteSchemaRelationship(
|
||||
dataSourceName,
|
||||
table,
|
||||
relationship
|
||||
);
|
||||
|
||||
// If its neither of those two cases, then it must be a legacy DB-to-RS relationship
|
||||
return adaptLegacyRemoteSchemaRelationship(
|
||||
dataSourceName,
|
||||
table,
|
||||
relationship
|
||||
);
|
||||
}
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Local Object relationships
|
||||
*/
|
||||
if (metadataTable.object_relationships) {
|
||||
relationships = [
|
||||
...relationships,
|
||||
...metadataTable.object_relationships.map<Relationship>(
|
||||
relationship => {
|
||||
if (isManualObjectRelationship(relationship))
|
||||
return adaptManualRelationship(
|
||||
dataSourceName,
|
||||
table,
|
||||
relationship
|
||||
);
|
||||
|
||||
/**
|
||||
* To a local table via FK relationships
|
||||
*/
|
||||
if (isLocalTableObjectRelationship(relationship))
|
||||
return adaptLocalTableRelationship(
|
||||
dataSourceName,
|
||||
table,
|
||||
relationship,
|
||||
fkRelationships
|
||||
);
|
||||
|
||||
/**
|
||||
* If its not both the above cases then its a object relationships between columns in the same table
|
||||
*/
|
||||
return adaptSameTableObjectRelationship(
|
||||
dataSourceName,
|
||||
table,
|
||||
relationship,
|
||||
fkRelationships
|
||||
);
|
||||
}
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Local Array relationships
|
||||
*/
|
||||
if (metadataTable.array_relationships) {
|
||||
relationships = [
|
||||
...relationships,
|
||||
...metadataTable.array_relationships.map<Relationship>(
|
||||
relationship => {
|
||||
if (isManualArrayRelationship(relationship))
|
||||
return adaptManualRelationship(
|
||||
dataSourceName,
|
||||
table,
|
||||
relationship
|
||||
);
|
||||
|
||||
return adaptLocalTableRelationship(
|
||||
dataSourceName,
|
||||
table,
|
||||
relationship,
|
||||
fkRelationships
|
||||
);
|
||||
}
|
||||
),
|
||||
];
|
||||
}
|
||||
return relationships;
|
||||
},
|
||||
});
|
||||
};
|
@ -0,0 +1,194 @@
|
||||
import {
|
||||
isLegacyFkConstraint,
|
||||
LegacyRemoteSchemaRelationship,
|
||||
LocalTableArrayRelationship,
|
||||
LocalTableObjectRelationship,
|
||||
ManualArrayRelationship,
|
||||
ManualObjectRelationship,
|
||||
RemoteDBRelationship,
|
||||
RemoteSchemaRelationship,
|
||||
SameTableObjectRelationship,
|
||||
Table,
|
||||
TableFkRelationships,
|
||||
} from '@/features/DataSource';
|
||||
import { getRemoteFieldPath } from '@/features/RelationshipsTable';
|
||||
import { Relationship } from '../../types';
|
||||
|
||||
export const getTargetColumns = (
|
||||
tableRlns: TableFkRelationships[],
|
||||
fkConstraint: string[]
|
||||
) => {
|
||||
const destination = tableRlns.find(tr => {
|
||||
return tr.from.column.find(c =>
|
||||
fkConstraint.includes(c?.replace(/"/g, ''))
|
||||
);
|
||||
});
|
||||
return destination?.to.column ?? [];
|
||||
};
|
||||
|
||||
export const adaptRemoteSchemaRelationship = (
|
||||
dataSourceName: string,
|
||||
table: Table,
|
||||
relationship: RemoteSchemaRelationship
|
||||
): Relationship & { type: 'toRemoteSchema' } => {
|
||||
return {
|
||||
name: relationship.name,
|
||||
type: 'toRemoteSchema',
|
||||
toRemoteSchema: relationship.definition.to_remote_schema.remote_schema,
|
||||
relationship_type: 'Remote Schema',
|
||||
mapping: {
|
||||
from: {
|
||||
source: dataSourceName,
|
||||
table,
|
||||
columns: relationship.definition.to_remote_schema.lhs_fields,
|
||||
},
|
||||
to: {
|
||||
remoteSchema: relationship.definition.to_remote_schema.remote_schema,
|
||||
fields: getRemoteFieldPath(
|
||||
relationship.definition.to_remote_schema.remote_field
|
||||
),
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const adaptLegacyRemoteSchemaRelationship = (
|
||||
dataSourceName: string,
|
||||
table: Table,
|
||||
relationship: LegacyRemoteSchemaRelationship
|
||||
): Relationship & { type: 'toRemoteSchema' } => {
|
||||
return {
|
||||
name: relationship.name,
|
||||
type: 'toRemoteSchema',
|
||||
toRemoteSchema: relationship.definition.remote_schema,
|
||||
relationship_type: 'Remote Schema',
|
||||
mapping: {
|
||||
from: {
|
||||
source: dataSourceName,
|
||||
table,
|
||||
columns: relationship.definition.hasura_fields,
|
||||
},
|
||||
to: {
|
||||
remoteSchema: relationship.definition.remote_schema,
|
||||
fields: getRemoteFieldPath(relationship.definition.remote_field),
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const adaptRemoteDBRelationship = (
|
||||
dataSourceName: string,
|
||||
table: Table,
|
||||
relationship: RemoteDBRelationship
|
||||
): Relationship & { type: 'toSource' } => {
|
||||
return {
|
||||
name: relationship.name,
|
||||
type: 'toSource',
|
||||
toSource: relationship.definition.to_source.source,
|
||||
relationship_type: relationship.definition.to_source.relationship_type,
|
||||
mapping: {
|
||||
from: {
|
||||
source: dataSourceName,
|
||||
table,
|
||||
columns: Object.keys(relationship.definition.to_source.field_mapping),
|
||||
},
|
||||
to: {
|
||||
source: relationship.definition.to_source.source,
|
||||
table: relationship.definition.to_source.table,
|
||||
columns: Object.values(relationship.definition.to_source.field_mapping),
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const adaptManualRelationship = (
|
||||
dataSourceName: string,
|
||||
table: Table,
|
||||
relationship: ManualObjectRelationship | ManualArrayRelationship
|
||||
): Relationship & { type: 'toLocalTableManual' } => {
|
||||
return {
|
||||
name: relationship.name,
|
||||
type: 'toLocalTableManual',
|
||||
toLocalTable: relationship.using.manual_configuration.remote_table,
|
||||
relationship_type: 'Object',
|
||||
mapping: {
|
||||
from: {
|
||||
source: dataSourceName,
|
||||
table,
|
||||
columns: Object.keys(
|
||||
relationship.using.manual_configuration.column_mapping
|
||||
),
|
||||
},
|
||||
to: {
|
||||
source: dataSourceName,
|
||||
table: relationship.using.manual_configuration.remote_table,
|
||||
columns: Object.values(
|
||||
relationship.using.manual_configuration.column_mapping
|
||||
),
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const adaptLocalTableRelationship = (
|
||||
dataSourceName: string,
|
||||
table: Table,
|
||||
relationship: LocalTableObjectRelationship | LocalTableArrayRelationship,
|
||||
fkRelationships: TableFkRelationships[]
|
||||
): Relationship & { type: 'toLocalTableFk' } => {
|
||||
const columns = isLegacyFkConstraint(
|
||||
relationship.using.foreign_key_constraint_on
|
||||
)
|
||||
? [relationship.using.foreign_key_constraint_on.column]
|
||||
: relationship.using.foreign_key_constraint_on.columns;
|
||||
|
||||
return {
|
||||
name: relationship.name,
|
||||
type: 'toLocalTableFk',
|
||||
toLocalTable: relationship.using.foreign_key_constraint_on.table,
|
||||
relationship_type: 'Object',
|
||||
mapping: {
|
||||
from: {
|
||||
source: dataSourceName,
|
||||
table,
|
||||
columns,
|
||||
},
|
||||
to: {
|
||||
source: dataSourceName,
|
||||
table: relationship.using.foreign_key_constraint_on.table,
|
||||
columns: getTargetColumns(fkRelationships, columns),
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const adaptSameTableObjectRelationship = (
|
||||
dataSourceName: string,
|
||||
table: Table,
|
||||
relationship: SameTableObjectRelationship,
|
||||
fkRelationships: TableFkRelationships[]
|
||||
): Relationship & { type: 'toSameTableFk' } => {
|
||||
const columns =
|
||||
typeof relationship.using.foreign_key_constraint_on === 'string'
|
||||
? [relationship.using.foreign_key_constraint_on]
|
||||
: relationship.using.foreign_key_constraint_on;
|
||||
|
||||
return {
|
||||
name: relationship.name,
|
||||
type: 'toSameTableFk',
|
||||
toLocalTable: table,
|
||||
relationship_type: 'Object',
|
||||
mapping: {
|
||||
from: {
|
||||
source: dataSourceName,
|
||||
table,
|
||||
columns,
|
||||
},
|
||||
to: {
|
||||
source: dataSourceName,
|
||||
table,
|
||||
columns: getTargetColumns(fkRelationships, columns),
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
@ -1,2 +1 @@
|
||||
export * from './DatabaseRelationshipTable';
|
||||
export { RowData } from './types';
|
||||
|
@ -1,45 +1,53 @@
|
||||
import {
|
||||
DbToDbRelationship,
|
||||
DbToRemoteSchemaRelationship,
|
||||
} from '@/features/MetadataAPI';
|
||||
import { ArrayRelationship, ObjectRelationship } from '@/metadata/types';
|
||||
import { Table } from '@/features/DataSource';
|
||||
|
||||
type BaseTypes = {
|
||||
type SourceDef = {
|
||||
source: string;
|
||||
table: Table;
|
||||
columns: string[];
|
||||
};
|
||||
|
||||
type RemoteSchemaDef = {
|
||||
remoteSchema: string;
|
||||
fields: string[];
|
||||
};
|
||||
|
||||
export type Relationship = {
|
||||
name: string;
|
||||
reference: string;
|
||||
referenceTable: string;
|
||||
target: string;
|
||||
targetTable?: string;
|
||||
fieldsFrom: string[];
|
||||
fieldsTo: string[];
|
||||
};
|
||||
|
||||
type DbToDb = {
|
||||
fromType: 'table';
|
||||
toType: 'database';
|
||||
type: 'Object' | 'Array';
|
||||
relationship: DbToDbRelationship;
|
||||
} & BaseTypes;
|
||||
|
||||
type LocalObject = {
|
||||
fromType: 'table';
|
||||
toType: 'table';
|
||||
type: 'Object';
|
||||
relationship: ObjectRelationship;
|
||||
};
|
||||
|
||||
type LocalArray = {
|
||||
fromType: 'table';
|
||||
toType: 'table';
|
||||
type: 'Array';
|
||||
relationship: ArrayRelationship;
|
||||
};
|
||||
|
||||
type Remote = {
|
||||
fromType: 'table';
|
||||
toType: 'remote_schema';
|
||||
type: 'Remote Schema';
|
||||
relationship: DbToRemoteSchemaRelationship;
|
||||
};
|
||||
|
||||
export type RowData = (DbToDb | LocalObject | LocalArray | Remote) & BaseTypes;
|
||||
} & (
|
||||
| {
|
||||
type: 'toLocalTableManual' | 'toLocalTableFk';
|
||||
toLocalTable: Table;
|
||||
relationship_type: 'Array' | 'Object';
|
||||
mapping: {
|
||||
from: SourceDef;
|
||||
to: SourceDef;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: 'toSameTableFk';
|
||||
toLocalTable: Table;
|
||||
relationship_type: 'Object';
|
||||
mapping: {
|
||||
from: SourceDef;
|
||||
to: SourceDef;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: 'toSource';
|
||||
toSource: string;
|
||||
relationship_type: 'array' | 'object';
|
||||
mapping: {
|
||||
from: SourceDef;
|
||||
to: SourceDef;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: 'toRemoteSchema';
|
||||
toRemoteSchema: string;
|
||||
relationship_type: 'Remote Schema';
|
||||
mapping: {
|
||||
from: SourceDef;
|
||||
to: RemoteSchemaDef;
|
||||
};
|
||||
}
|
||||
);
|
||||
|
@ -1,3 +1,5 @@
|
||||
export type { ExistingRelationshipMeta } from './RemoteSchemaRelationshipsTable';
|
||||
export { RemoteSchemaRelationshipTable } from './RemoteSchemaRelationshipsTable';
|
||||
export type { RowData } from './DatabaseRelationshipsTable/types';
|
||||
export { Relationship } from './DatabaseRelationshipsTable/types';
|
||||
export { useFindRelationship } from './DatabaseRelationshipsTable/hooks/useFindRelationship';
|
||||
export { getRemoteFieldPath } from './utils';
|
||||
|
@ -1,4 +1,6 @@
|
||||
import { RemoteRelationshipFieldServer } from '@/components/Services/Data/TableRelationships/RemoteRelationships/utils';
|
||||
import isEqual from 'lodash.isequal';
|
||||
import { Table } from '../DataSource';
|
||||
import { RelationshipSourceType, RelationshipType } from './types';
|
||||
|
||||
export const getRemoteRelationType = (
|
||||
@ -60,3 +62,10 @@ export const getRemoteSchemaRelationType = (
|
||||
'Remote Schema',
|
||||
];
|
||||
};
|
||||
|
||||
export const areTablesEqual = (table1: Table, table2: Table) => {
|
||||
const values1 = Object.values(table1 as any).sort();
|
||||
const values2 = Object.values(table2 as any).sort();
|
||||
|
||||
return isEqual(values1, values2);
|
||||
};
|
||||
|
@ -4,7 +4,6 @@ import type { MetadataDataSource } from '@/metadata/types';
|
||||
import uniqueId from 'lodash.uniqueid';
|
||||
import { useQuery } from 'react-query';
|
||||
import { useHttpClient } from '@/features/Network';
|
||||
import { useAppSelector } from '@/store';
|
||||
|
||||
type DataSource = Pick<MetadataDataSource, 'name'>;
|
||||
|
||||
@ -76,7 +75,7 @@ const getTrackableTables = (
|
||||
) =>
|
||||
introspectedTables.map(introspectedTable => {
|
||||
const trackedTable = trackedTables.find(
|
||||
_trackedTable =>
|
||||
(_trackedTable: any) =>
|
||||
`${_trackedTable.table.schema}.${_trackedTable.table.name}` ===
|
||||
introspectedTable.name
|
||||
);
|
||||
@ -107,8 +106,7 @@ const getTrackableTables = (
|
||||
});
|
||||
|
||||
export const useTables = ({ dataSource }: UseTablesProps) => {
|
||||
const headers = useAppSelector(state => state.tables.dataHeaders);
|
||||
const httpClient = useHttpClient({ headers });
|
||||
const httpClient = useHttpClient();
|
||||
return useQuery<TrackableTable[], Error>({
|
||||
queryKey: [dataSource.name, 'tables'],
|
||||
queryFn: async () => {
|
||||
|
Loading…
Reference in New Issue
Block a user