console: Suggested relationship foreign key constraint on

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/7880
GitOrigin-RevId: 24d2af47d308399ce489cda5bcf888b9d8a80fc5
This commit is contained in:
Luca Restagno 2023-02-15 13:05:57 +01:00 committed by hasura-bot
parent 01db431943
commit 2b4acb11e7
15 changed files with 162 additions and 125 deletions

View File

@ -1,10 +1,10 @@
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
import Endpoints from '@/Endpoints'; import Endpoints from '@/Endpoints';
import { areTablesEqual } from '@/features/RelationshipsTable';
import { useHttpClient } from '@/features/Network'; import { useHttpClient } from '@/features/Network';
import { useQuery, UseQueryResult } from 'react-query'; import { useQuery, UseQueryResult } from 'react-query';
import { useTablesForeignKeys } from './useTableForeignKeys'; import { useTablesForeignKeys } from './useTableForeignKeys';
import { TableObject, ForeignKeyMapping } from '../types'; import { TableObject, ForeignKeyMapping } from '../types';
import { areTablesEqual } from '@/features/hasura-metadata-api';
export type UseTableEnumOptionsProps = { export type UseTableEnumOptionsProps = {
tables: TableObject[]; tables: TableObject[];

View File

@ -9,6 +9,7 @@ import {
} from '@/features/FeatureFlags'; } from '@/features/FeatureFlags';
import { DatabaseRelationshipsTab } from '@/features/DatabaseRelationships'; import { DatabaseRelationshipsTab } from '@/features/DatabaseRelationships';
import { Button } from '@/new-components/Button'; import { Button } from '@/new-components/Button';
import { areTablesEqual } from '@/features/hasura-metadata-api';
import { LearnMoreLink } from '@/new-components/LearnMoreLink'; import { LearnMoreLink } from '@/new-components/LearnMoreLink';
import TableHeader from '../TableCommon/TableHeader'; import TableHeader from '../TableCommon/TableHeader';
import { import {
@ -43,7 +44,6 @@ import { getRemoteSchemasSelector } from '../../../../metadata/selector';
import { RightContainer } from '../../../Common/Layout/RightContainer'; import { RightContainer } from '../../../Common/Layout/RightContainer';
import FeatureDisabled from '../FeatureDisabled'; import FeatureDisabled from '../FeatureDisabled';
import { RemoteDbRelationships } from './RemoteDbRelationships/RemoteDbRelationships'; import { RemoteDbRelationships } from './RemoteDbRelationships/RemoteDbRelationships';
import { areTablesEqual } from '@/features/RelationshipsTable/utils';
const addRelationshipCellView = ( const addRelationshipCellView = (
dispatch, dispatch,

View File

@ -1,6 +1,5 @@
/* eslint-disable no-underscore-dangle */ /* eslint-disable no-underscore-dangle */
import { getScalarType, getTypeName } from '@/features/GraphQLUtils'; import { getScalarType, getTypeName } from '@/features/GraphQLUtils';
import { areTablesEqual } from '@/features/RelationshipsTable';
import { GraphQLType } from 'graphql'; import { GraphQLType } from 'graphql';
import { GDCTable } from '..'; import { GDCTable } from '..';
import { import {
@ -11,6 +10,7 @@ import {
import { GetTableColumnsProps, TableColumn } from '../../types'; import { GetTableColumnsProps, TableColumn } from '../../types';
import { adaptAgentDataType } from './utils'; import { adaptAgentDataType } from './utils';
import { GetTableInfoResponse } from './types'; import { GetTableInfoResponse } from './types';
import { areTablesEqual } from '@/features/hasura-metadata-api';
export const getTableColumns = async (props: GetTableColumnsProps) => { export const getTableColumns = async (props: GetTableColumnsProps) => {
const { httpClient, dataSourceName, table } = props; const { httpClient, dataSourceName, table } = props;

View File

@ -5,7 +5,6 @@ import {
FaArrowRight, FaArrowRight,
FaColumns, FaColumns,
FaFont, FaFont,
FaExclamationTriangle,
FaPlug, FaPlug,
FaTable, FaTable,
} from 'react-icons/fa'; } from 'react-icons/fa';
@ -21,16 +20,14 @@ const Columns = ({
}) => { }) => {
const isMappingPresent = Object.entries(mapping)?.length ?? undefined; const isMappingPresent = Object.entries(mapping)?.length ?? undefined;
return isMappingPresent ? ( if (!isMappingPresent) {
<> return <></>;
{type === 'from' }
? Object.keys(mapping).join(',')
: Object.values(mapping).join(',')} return type === 'from' ? (
</> <>{Object.keys(mapping).join(',')}</>
) : ( ) : (
<Tooltip tooltipContentChildren="Unable to retrieve any column info. Please check if your datasource is reachable."> <>{Object.values(mapping).join(',')}</>
<FaExclamationTriangle className="text-red-600" />
</Tooltip>
); );
}; };
@ -39,6 +36,15 @@ export const RelationshipMapping = ({
}: { }: {
relationship: Relationship; relationship: Relationship;
}) => { }) => {
if (relationship.type !== 'remoteSchemaRelationship') {
const isMappingPresent =
Object.entries(relationship.definition?.mapping)?.length ?? undefined;
if (!isMappingPresent) {
return null;
}
}
return ( return (
<div className="flex items-center gap-6"> <div className="flex items-center gap-6">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">

View File

@ -3,7 +3,7 @@ import { capitaliseFirstLetter } from '@/components/Common/ConfigureTransformati
import { Table } from '@/features/hasura-metadata-types'; import { Table } from '@/features/hasura-metadata-types';
import { Button } from '@/new-components/Button'; import { Button } from '@/new-components/Button';
import { CardedTable } from '@/new-components/CardedTable'; import { CardedTable } from '@/new-components/CardedTable';
import { useFireNotification } from '@/new-components/Notifications'; import { hasuraToast } from '@/new-components/Toasts';
import { import {
FaArrowRight, FaArrowRight,
FaColumns, FaColumns,
@ -14,13 +14,11 @@ import {
import { MetadataSelectors, useMetadata } from '@/features/hasura-metadata-api'; import { MetadataSelectors, useMetadata } from '@/features/hasura-metadata-api';
import { getSupportsForeignKeys } from '@/features/hasura-metadata-api/utils'; import { getSupportsForeignKeys } from '@/features/hasura-metadata-api/utils';
import { useListAllDatabaseRelationships } from '../../hooks/useListAllDatabaseRelationships'; import { useListAllDatabaseRelationships } from '../../hooks/useListAllDatabaseRelationships';
import { useManageLocalRelationship } from '../../hooks/useManageLocalRelationship';
import { getTableDisplayName } from '../../utils/helpers'; import { getTableDisplayName } from '../../utils/helpers';
import { import {
SuggestedRelationshipWithName, SuggestedRelationshipWithName,
useSuggestedRelationships, useSuggestedRelationships,
} from './hooks/useSuggestedRelationships'; } from './hooks/useSuggestedRelationships';
import { convertSuggestedRelationShipToLocalRelationship } from './SuggestedRelationships.utils';
import type { LocalRelationship } from '../../types'; import type { LocalRelationship } from '../../types';
import Skeleton from 'react-loading-skeleton'; import Skeleton from 'react-loading-skeleton';
@ -37,7 +35,6 @@ export const SuggestedRelationships = ({
dataSourceName, dataSourceName,
table, table,
}); });
const localRelationships = existingRelationships.filter(rel => { const localRelationships = existingRelationships.filter(rel => {
if (rel.type === 'localRelationship') { if (rel.type === 'localRelationship') {
return true; return true;
@ -55,6 +52,8 @@ export const SuggestedRelationships = ({
suggestedRelationships, suggestedRelationships,
isLoadingSuggestedRelationships, isLoadingSuggestedRelationships,
refetchSuggestedRelationships, refetchSuggestedRelationships,
onAddSuggestedRelationship,
isAddingSuggestedRelationship,
} = useSuggestedRelationships({ } = useSuggestedRelationships({
dataSourceName, dataSourceName,
table, table,
@ -62,40 +61,35 @@ export const SuggestedRelationships = ({
isEnabled: supportsForeignKeys, isEnabled: supportsForeignKeys,
}); });
const { fireNotification } = useFireNotification();
const { createRelationship, isLoading: isCreatingRelationship } =
useManageLocalRelationship({
dataSourceName,
table,
onSuccess: () => {
fireNotification({
title: 'Success',
message: 'Relationship tracked',
type: 'success',
});
refetchSuggestedRelationships();
},
onError: () => {
fireNotification({
title: 'Error',
message: 'An error occurred',
type: 'error',
});
},
});
const [updatedRelationship, setUpdatedRelationship] = useState<string | null>( const [updatedRelationship, setUpdatedRelationship] = useState<string | null>(
null null
); );
const onCreate = (relationship: SuggestedRelationshipWithName) => { const onCreate = async (relationship: SuggestedRelationshipWithName) => {
setUpdatedRelationship(relationship.constraintName); setUpdatedRelationship(relationship.constraintName);
createRelationship( try {
convertSuggestedRelationShipToLocalRelationship( const isObjectRelationship = !!relationship.from?.constraint_name;
dataSourceName,
relationship await onAddSuggestedRelationship({
) name: relationship.constraintName,
); columnNames: isObjectRelationship
? relationship.from.columns
: relationship.to.columns,
relationshipType: isObjectRelationship ? 'object' : 'array',
toTable: isObjectRelationship ? undefined : relationship.to.table,
});
hasuraToast({
title: 'Success',
message: 'Relationship tracked',
type: 'success',
});
refetchSuggestedRelationships();
} catch (err) {
hasuraToast({
title: 'Error',
message: 'An error occurred',
type: 'error',
});
}
}; };
if (isLoadingSuggestedRelationships) if (isLoadingSuggestedRelationships)
@ -125,7 +119,7 @@ export const SuggestedRelationships = ({
size="sm" size="sm"
onClick={() => onCreate(relationship)} onClick={() => onCreate(relationship)}
isLoading={ isLoading={
isCreatingRelationship && isAddingSuggestedRelationship &&
updatedRelationship === relationship.constraintName updatedRelationship === relationship.constraintName
} }
> >

View File

@ -5,52 +5,18 @@ import {
SuggestedRelationship, SuggestedRelationship,
} from '@/features/DatabaseRelationships/types'; } from '@/features/DatabaseRelationships/types';
import { getTableDisplayName } from '@/features/DatabaseRelationships/utils/helpers'; import { getTableDisplayName } from '@/features/DatabaseRelationships/utils/helpers';
import { getDriverPrefix, NetworkArgs } from '@/features/DataSource'; import { getDriverPrefix, runMetadataQuery } from '@/features/DataSource';
import { import {
areTablesEqual, areTablesEqual,
MetadataSelectors, MetadataSelectors,
} from '@/features/hasura-metadata-api'; } from '@/features/hasura-metadata-api';
import { import { useMetadata } from '@/features/hasura-metadata-api/useMetadata';
DEFAULT_STALE_TIME,
useMetadata,
} from '@/features/hasura-metadata-api/useMetadata';
import { Table } from '@/features/hasura-metadata-types'; import { Table } from '@/features/hasura-metadata-types';
import { useHttpClient } from '@/features/Network'; import { useHttpClient } from '@/features/Network';
import { useEffect } from 'react'; import { useEffect, useState } from 'react';
import { useQuery } from 'react-query'; import { useQuery, useQueryClient } from 'react-query';
import { generateQueryKeys } from '@/features/DatabaseRelationships/utils/queryClientUtils';
type FetchSuggestedRelationshipsArgs = NetworkArgs & { import { useMetadataMigration } from '@/features/MetadataAPI';
dataSourceName: string;
table: Table;
driverPrefix?: string;
};
const emptyResponse: SuggestedRelationshipsResponse = { relationships: [] };
const fetchSuggestedRelationships = async ({
httpClient,
dataSourceName,
table,
driverPrefix,
}: FetchSuggestedRelationshipsArgs) => {
if (!driverPrefix) {
return Promise.resolve(emptyResponse);
}
return (
await httpClient.post<any, { data: SuggestedRelationshipsResponse }>(
'/v1/metadata',
{
type: `${driverPrefix}_suggest_relationships`,
version: 1,
args: {
omit_tracked: true,
tables: [table],
source: dataSourceName,
},
}
)
).data;
};
type UseSuggestedRelationshipsArgs = { type UseSuggestedRelationshipsArgs = {
dataSourceName: string; dataSourceName: string;
@ -59,7 +25,7 @@ type UseSuggestedRelationshipsArgs = {
isEnabled: boolean; isEnabled: boolean;
}; };
type SuggestedRelationshipsResponse = { export type SuggestedRelationshipsResponse = {
relationships: SuggestedRelationship[]; relationships: SuggestedRelationship[];
}; };
@ -187,35 +153,86 @@ export const useSuggestedRelationships = ({
MetadataSelectors.findSource(dataSourceName) MetadataSelectors.findSource(dataSourceName)
); );
const metadataMutation = useMetadataMigration({});
const queryClient = useQueryClient();
const dataSourcePrefix = metadataSource?.kind const dataSourcePrefix = metadataSource?.kind
? getDriverPrefix(metadataSource?.kind) ? getDriverPrefix(metadataSource?.kind)
: undefined; : undefined;
const httpClient = useHttpClient(); const httpClient = useHttpClient();
const { data, refetch, isLoading } = useQuery<SuggestedRelationshipsResponse>(
{
queryKey: ['suggested_relationships', dataSourceName, table],
queryFn: async () => {
if (!isEnabled) {
return Promise.resolve(emptyResponse);
}
const result = await fetchSuggestedRelationships({ const {
httpClient, data,
dataSourceName, refetch: refetchSuggestedRelationships,
isLoading: isLoadingSuggestedRelationships,
} = useQuery({
queryKey: ['suggested_relationships', dataSourceName, table],
queryFn: async () => {
const body = {
type: `${dataSourcePrefix}_suggest_relationships`,
args: {
omit_tracked: true,
tables: [table],
source: dataSourceName,
},
};
const result = await runMetadataQuery<SuggestedRelationshipsResponse>({
httpClient,
body,
});
return result;
},
enabled: isEnabled,
});
const [isAddingSuggestedRelationship, setAddingSuggestedRelationship] =
useState(false);
const onAddSuggestedRelationship = async ({
name,
columnNames,
relationshipType,
toTable,
}: {
name: string;
columnNames: string[];
relationshipType: 'object' | 'array';
toTable?: Table;
}) => {
setAddingSuggestedRelationship(true);
await metadataMutation.mutateAsync({
query: {
type: `${dataSourcePrefix}_create_${relationshipType}_relationship`,
args: {
table, table,
driverPrefix: dataSourcePrefix, name,
}); source: dataSourceName,
return result; using: {
foreign_key_constraint_on:
relationshipType === 'object'
? columnNames
: {
table: toTable,
columns: columnNames,
},
},
},
}, },
refetchOnWindowFocus: false, });
staleTime: DEFAULT_STALE_TIME, setAddingSuggestedRelationship(false);
}
); queryClient.invalidateQueries({
queryKey: generateQueryKeys.metadata(),
});
};
useEffect(() => { useEffect(() => {
if (dataSourcePrefix) { if (dataSourcePrefix) {
refetch(); refetchSuggestedRelationships();
} }
}, [dataSourcePrefix]); }, [dataSourcePrefix]);
@ -238,7 +255,9 @@ export const useSuggestedRelationships = ({
return { return {
suggestedRelationships: relationshipsWithConstraintName, suggestedRelationships: relationshipsWithConstraintName,
isLoadingSuggestedRelationships: isLoading, isLoadingSuggestedRelationships,
refetchSuggestedRelationships: refetch, refetchSuggestedRelationships,
onAddSuggestedRelationship,
isAddingSuggestedRelationship,
}; };
}; };

View File

@ -2,6 +2,7 @@ import {
isSameTableObjectRelationship, isSameTableObjectRelationship,
TableFkRelationships, TableFkRelationships,
} from '@/features/DataSource'; } from '@/features/DataSource';
import { areTablesEqual } from '@/features/hasura-metadata-api';
import { import {
Legacy_SourceToRemoteSchemaRelationship, Legacy_SourceToRemoteSchemaRelationship,
LocalTableArrayRelationship, LocalTableArrayRelationship,
@ -13,7 +14,6 @@ import {
SourceToSourceRelationship, SourceToSourceRelationship,
Table, Table,
} from '@/features/hasura-metadata-types'; } from '@/features/hasura-metadata-types';
import { areTablesEqual } from '@/features/RelationshipsTable';
import isEqual from 'lodash.isequal'; import isEqual from 'lodash.isequal';
import { import {
LocalRelationship, LocalRelationship,

View File

@ -5,7 +5,7 @@ import { Table } from '@/features/hasura-metadata-types';
import { useHttpClient } from '@/features/Network'; import { useHttpClient } from '@/features/Network';
import { useQuery } from 'react-query'; import { useQuery } from 'react-query';
import { DataSource, exportMetadata, Operator } from '@/features/DataSource'; import { DataSource, exportMetadata, Operator } from '@/features/DataSource';
import { areTablesEqual } from '@/features/RelationshipsTable'; import { areTablesEqual } from '@/features/hasura-metadata-api';
import { getTypeName } from '@/features/GraphQLUtils'; import { getTypeName } from '@/features/GraphQLUtils';
import { InputField } from '@/new-components/Form'; import { InputField } from '@/new-components/Form';
import { IconTooltip } from '@/new-components/Tooltip'; import { IconTooltip } from '@/new-components/Tooltip';

View File

@ -4,8 +4,8 @@ import { useHttpClient } from '@/features/Network';
import { exportMetadata, runIntrospectionQuery } from '@/features/DataSource'; import { exportMetadata, runIntrospectionQuery } from '@/features/DataSource';
import { Table } from '@/features/hasura-metadata-types'; import { Table } from '@/features/hasura-metadata-types';
import { useQuery } from 'react-query'; import { useQuery } from 'react-query';
import { areTablesEqual } from '@/features/RelationshipsTable';
import { getAllColumnsAndOperators } from '../utils'; import { getAllColumnsAndOperators } from '../utils';
import { areTablesEqual } from '@/features/hasura-metadata-api';
/** /**
* *

View File

@ -1,4 +1,3 @@
export type { ExistingRelationshipMeta } from './RemoteSchemaRelationshipsTable'; export type { ExistingRelationshipMeta } from './RemoteSchemaRelationshipsTable';
export { RemoteSchemaRelationshipTable } from './RemoteSchemaRelationshipsTable'; export { RemoteSchemaRelationshipTable } from './RemoteSchemaRelationshipsTable';
export { getRemoteFieldPath } from './utils'; export { getRemoteFieldPath } from './utils';
export { areTablesEqual } from './utils';

View File

@ -2,6 +2,7 @@ import { RemoteRelationshipFieldServer } from '@/components/Services/Data/TableR
import isEqual from 'lodash.isequal'; import isEqual from 'lodash.isequal';
import { Table } from '@/features/hasura-metadata-types'; import { Table } from '@/features/hasura-metadata-types';
import { RelationshipSourceType, RelationshipType } from './types'; import { RelationshipSourceType, RelationshipType } from './types';
import { isArray, isObject } from '@/components/Common/utils/jsUtils';
export const getRemoteRelationType = ( export const getRemoteRelationType = (
relation: RelationshipType relation: RelationshipType
@ -62,10 +63,3 @@ export const getRemoteSchemaRelationType = (
'Remote Schema', '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

@ -0,0 +1,18 @@
import { areTablesEqual } from './areTablesEqual';
describe('areTablesEqual', () => {
it.each`
table1 | table2 | expected
${undefined} | ${undefined} | ${true}
${null} | ${undefined} | ${false}
${1} | ${2} | ${false}
${['Album']} | ${['Album']} | ${true}
${{ name: 'Album', schema: 'public' }} | ${{ name: 'Artist', schema: 'public' }} | ${false}
${{ name: 'Album', schema: 'public' }} | ${{ name: 'Album', schema: 'public' }} | ${true}
`(
'returns $expected when table1 is $table1 and table2 is $table2',
({ table1, table2, expected }) => {
expect(areTablesEqual(table1, table2)).toEqual(expected);
}
);
});

View File

@ -1,11 +1,17 @@
import { isArray, isObject } from '@/components/Common/utils/jsUtils';
import { Table } from '@/features/hasura-metadata-types'; import { Table } from '@/features/hasura-metadata-types';
import isEqual from 'lodash.isequal'; import isEqual from 'lodash.isequal';
// giving this it's own module so it doesn't get mixed up with utils const isObjectOrArray = (table: Table) => isObject(table) || isArray(table);
export const areTablesEqual = (table1: Table, table2: Table) => { export const areTablesEqual = (table1: Table, table2: Table) => {
const values1 = Object.values(table1 as any).sort(); const values1 = isObjectOrArray(table1)
const values2 = Object.values(table2 as any).sort(); ? Object.values(table1 as any).sort()
: table1;
const values2 = isObjectOrArray(table2)
? Object.values(table2 as any).sort()
: table2;
return isEqual(values1, values2); return isEqual(values1, values2);
}; };

View File

@ -2,8 +2,8 @@ import { useQuery } from 'react-query';
import { exportMetadata } from '@/features/DataSource'; import { exportMetadata } from '@/features/DataSource';
import { Table } from '@/features/hasura-metadata-types'; import { Table } from '@/features/hasura-metadata-types';
import { useHttpClient } from '@/features/Network'; import { useHttpClient } from '@/features/Network';
import { areTablesEqual } from '@/features/RelationshipsTable';
import { DEFAULT_STALE_TIME } from '../DatabaseRelationships'; import { DEFAULT_STALE_TIME } from '../DatabaseRelationships';
import { areTablesEqual } from './areTablesEqual';
export const useMetadata = () => { export const useMetadata = () => {
const httpClient = useHttpClient(); const httpClient = useHttpClient();

View File

@ -102,6 +102,7 @@ export const metadataQueryTypes = [
'drop_host_from_tls_allowlist', 'drop_host_from_tls_allowlist',
'dc_add_agent', 'dc_add_agent',
'dc_delete_agent', 'dc_delete_agent',
'suggest_relationships',
] as const; ] as const;
export type MetadataQueryType = (typeof metadataQueryTypes)[number]; export type MetadataQueryType = (typeof metadataQueryTypes)[number];