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:
Vijay Prasanna 2022-08-22 11:48:35 +05:30 committed by hasura-bot
parent a441308938
commit c0cff3c908
49 changed files with 1854 additions and 545 deletions

View File

@ -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",

View File

@ -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",

View File

@ -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,
};
};

View File

@ -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} />
);

View File

@ -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;

View File

@ -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',

View File

@ -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;
}[];
};

View File

@ -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,
};
};

View File

@ -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>

View File

@ -29,6 +29,7 @@ const table: NormalizedTable = {
export const Primary = () => (
<DatabaseRelationshipsTab
table={table}
metadataTable={{ name: 'user', schema: 'public' }}
currentSource="default"
migrationMode
driver="postgres"

View File

@ -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>
);

View File

@ -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}
/>
);
};

View File

@ -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'],
},
},
},

View File

@ -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;
/**

View File

@ -13,5 +13,6 @@ export const bigquery: Database = {
return ['dataset', 'name'];
},
getTableColumns,
getFKRelationships: async () => Feature.NotImplemented,
},
};

View File

@ -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,
},
};

View File

@ -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);
};

View File

@ -1 +1,2 @@
export { getTableColumns } from './getTableColumns';
export { getFKRelationships } from './getFKRelationships';

View File

@ -17,5 +17,6 @@ export const gdc: Database = {
getTableColumns: async () => {
return Feature.NotImplemented;
},
getFKRelationships: async () => Feature.NotImplemented,
},
};

View 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;
};

View File

@ -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';

View File

@ -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,
},
};

View File

@ -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);
};

View File

@ -1 +1,2 @@
export { getTableColumns } from './getTableColumns';
export { getFKRelationships } from './getFKRelationships';

View File

@ -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,
},
};

View File

@ -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);
};

View File

@ -1,3 +1,4 @@
export { getTrackableTables } from './getTrackableTables';
export { getDatabaseConfiguration } from './getDatabaseConfiguration';
export { getTableColumns } from './getTableColumns';
export { getFKRelationships } from './getFKRelationships';

View File

@ -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 };

View File

@ -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

View File

@ -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,

View File

@ -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' },
};

View File

@ -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>
))}

View File

@ -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'],
},
},
};

View File

@ -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);
});
});

View File

@ -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}>
&nbsp;/
<TableRowIcon type={secondaryIconToType} />
{field}
</React.Fragment>
))}
</div>
</div>
);
};

View File

@ -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>
);

View File

@ -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}>
&nbsp;/
<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}>
&nbsp;/
<TableRowIcon type={secondaryIconToType} />
{field}
</React.Fragment>
))}
</div>
</div>
);
};

View File

@ -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}>
&nbsp;/
<TableRowIcon type={secondaryIconFromType} />
{field}
</React.Fragment>
))}
</div>
</div>
);
};

View File

@ -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;
}
};

View File

@ -1,5 +0,0 @@
export * from './ModifyActions';
export * from './RelationshipsCell';
export * from './SourceRelationshipsCell';
export * from './DestinationRelationshipCell';
export * from './TableRowIcon';

View File

@ -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;
},
});
};

View File

@ -0,0 +1 @@
export { useListAllRelationshipsFromMetadata } from './useListAllRelationshipsFromMetadata';

View File

@ -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;
},
});
};

View File

@ -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),
},
},
};
};

View File

@ -1,2 +1 @@
export * from './DatabaseRelationshipTable';
export { RowData } from './types';

View File

@ -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;
};
}
);

View File

@ -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';

View File

@ -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);
};

View File

@ -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 () => {