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
import Endpoints from '@/Endpoints';
import { areTablesEqual } from '@/features/RelationshipsTable';
import { useHttpClient } from '@/features/Network';
import { useQuery, UseQueryResult } from 'react-query';
import { useTablesForeignKeys } from './useTableForeignKeys';
import { TableObject, ForeignKeyMapping } from '../types';
import { areTablesEqual } from '@/features/hasura-metadata-api';
export type UseTableEnumOptionsProps = {
tables: TableObject[];

View File

@ -9,6 +9,7 @@ import {
} from '@/features/FeatureFlags';
import { DatabaseRelationshipsTab } from '@/features/DatabaseRelationships';
import { Button } from '@/new-components/Button';
import { areTablesEqual } from '@/features/hasura-metadata-api';
import { LearnMoreLink } from '@/new-components/LearnMoreLink';
import TableHeader from '../TableCommon/TableHeader';
import {
@ -43,7 +44,6 @@ 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,

View File

@ -1,6 +1,5 @@
/* eslint-disable no-underscore-dangle */
import { getScalarType, getTypeName } from '@/features/GraphQLUtils';
import { areTablesEqual } from '@/features/RelationshipsTable';
import { GraphQLType } from 'graphql';
import { GDCTable } from '..';
import {
@ -11,6 +10,7 @@ import {
import { GetTableColumnsProps, TableColumn } from '../../types';
import { adaptAgentDataType } from './utils';
import { GetTableInfoResponse } from './types';
import { areTablesEqual } from '@/features/hasura-metadata-api';
export const getTableColumns = async (props: GetTableColumnsProps) => {
const { httpClient, dataSourceName, table } = props;
@ -80,7 +80,7 @@ export const getTableColumns = async (props: GetTableColumnsProps) => {
name: column.name,
dataType: adaptAgentDataType(column.type),
/**
Will be updated once GDC supports mutations
Will be updated once GDC supports mutations
*/
consoleDataType: 'string',
nullable: column.nullable,

View File

@ -5,7 +5,6 @@ import {
FaArrowRight,
FaColumns,
FaFont,
FaExclamationTriangle,
FaPlug,
FaTable,
} from 'react-icons/fa';
@ -21,16 +20,14 @@ const Columns = ({
}) => {
const isMappingPresent = Object.entries(mapping)?.length ?? undefined;
return isMappingPresent ? (
<>
{type === 'from'
? Object.keys(mapping).join(',')
: Object.values(mapping).join(',')}
</>
if (!isMappingPresent) {
return <></>;
}
return type === 'from' ? (
<>{Object.keys(mapping).join(',')}</>
) : (
<Tooltip tooltipContentChildren="Unable to retrieve any column info. Please check if your datasource is reachable.">
<FaExclamationTriangle className="text-red-600" />
</Tooltip>
<>{Object.values(mapping).join(',')}</>
);
};
@ -39,6 +36,15 @@ export const RelationshipMapping = ({
}: {
relationship: Relationship;
}) => {
if (relationship.type !== 'remoteSchemaRelationship') {
const isMappingPresent =
Object.entries(relationship.definition?.mapping)?.length ?? undefined;
if (!isMappingPresent) {
return null;
}
}
return (
<div className="flex items-center gap-6">
<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 { Button } from '@/new-components/Button';
import { CardedTable } from '@/new-components/CardedTable';
import { useFireNotification } from '@/new-components/Notifications';
import { hasuraToast } from '@/new-components/Toasts';
import {
FaArrowRight,
FaColumns,
@ -14,13 +14,11 @@ import {
import { MetadataSelectors, useMetadata } from '@/features/hasura-metadata-api';
import { getSupportsForeignKeys } from '@/features/hasura-metadata-api/utils';
import { useListAllDatabaseRelationships } from '../../hooks/useListAllDatabaseRelationships';
import { useManageLocalRelationship } from '../../hooks/useManageLocalRelationship';
import { getTableDisplayName } from '../../utils/helpers';
import {
SuggestedRelationshipWithName,
useSuggestedRelationships,
} from './hooks/useSuggestedRelationships';
import { convertSuggestedRelationShipToLocalRelationship } from './SuggestedRelationships.utils';
import type { LocalRelationship } from '../../types';
import Skeleton from 'react-loading-skeleton';
@ -37,7 +35,6 @@ export const SuggestedRelationships = ({
dataSourceName,
table,
});
const localRelationships = existingRelationships.filter(rel => {
if (rel.type === 'localRelationship') {
return true;
@ -55,6 +52,8 @@ export const SuggestedRelationships = ({
suggestedRelationships,
isLoadingSuggestedRelationships,
refetchSuggestedRelationships,
onAddSuggestedRelationship,
isAddingSuggestedRelationship,
} = useSuggestedRelationships({
dataSourceName,
table,
@ -62,40 +61,35 @@ export const SuggestedRelationships = ({
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>(
null
);
const onCreate = (relationship: SuggestedRelationshipWithName) => {
const onCreate = async (relationship: SuggestedRelationshipWithName) => {
setUpdatedRelationship(relationship.constraintName);
createRelationship(
convertSuggestedRelationShipToLocalRelationship(
dataSourceName,
relationship
)
);
try {
const isObjectRelationship = !!relationship.from?.constraint_name;
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)
@ -125,7 +119,7 @@ export const SuggestedRelationships = ({
size="sm"
onClick={() => onCreate(relationship)}
isLoading={
isCreatingRelationship &&
isAddingSuggestedRelationship &&
updatedRelationship === relationship.constraintName
}
>

View File

@ -5,52 +5,18 @@ import {
SuggestedRelationship,
} from '@/features/DatabaseRelationships/types';
import { getTableDisplayName } from '@/features/DatabaseRelationships/utils/helpers';
import { getDriverPrefix, NetworkArgs } from '@/features/DataSource';
import { getDriverPrefix, runMetadataQuery } from '@/features/DataSource';
import {
areTablesEqual,
MetadataSelectors,
} from '@/features/hasura-metadata-api';
import {
DEFAULT_STALE_TIME,
useMetadata,
} from '@/features/hasura-metadata-api/useMetadata';
import { useMetadata } from '@/features/hasura-metadata-api/useMetadata';
import { Table } from '@/features/hasura-metadata-types';
import { useHttpClient } from '@/features/Network';
import { useEffect } from 'react';
import { useQuery } from 'react-query';
type FetchSuggestedRelationshipsArgs = NetworkArgs & {
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;
};
import { useEffect, useState } from 'react';
import { useQuery, useQueryClient } from 'react-query';
import { generateQueryKeys } from '@/features/DatabaseRelationships/utils/queryClientUtils';
import { useMetadataMigration } from '@/features/MetadataAPI';
type UseSuggestedRelationshipsArgs = {
dataSourceName: string;
@ -59,7 +25,7 @@ type UseSuggestedRelationshipsArgs = {
isEnabled: boolean;
};
type SuggestedRelationshipsResponse = {
export type SuggestedRelationshipsResponse = {
relationships: SuggestedRelationship[];
};
@ -187,35 +153,86 @@ export const useSuggestedRelationships = ({
MetadataSelectors.findSource(dataSourceName)
);
const metadataMutation = useMetadataMigration({});
const queryClient = useQueryClient();
const dataSourcePrefix = metadataSource?.kind
? getDriverPrefix(metadataSource?.kind)
: undefined;
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({
httpClient,
dataSourceName,
const {
data,
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,
driverPrefix: dataSourcePrefix,
});
return result;
name,
source: dataSourceName,
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(() => {
if (dataSourcePrefix) {
refetch();
refetchSuggestedRelationships();
}
}, [dataSourcePrefix]);
@ -238,7 +255,9 @@ export const useSuggestedRelationships = ({
return {
suggestedRelationships: relationshipsWithConstraintName,
isLoadingSuggestedRelationships: isLoading,
refetchSuggestedRelationships: refetch,
isLoadingSuggestedRelationships,
refetchSuggestedRelationships,
onAddSuggestedRelationship,
isAddingSuggestedRelationship,
};
};

View File

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

View File

@ -5,7 +5,7 @@ import { Table } from '@/features/hasura-metadata-types';
import { useHttpClient } from '@/features/Network';
import { useQuery } from 'react-query';
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 { InputField } from '@/new-components/Form';
import { IconTooltip } from '@/new-components/Tooltip';

View File

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

View File

@ -1,4 +1,3 @@
export type { ExistingRelationshipMeta } from './RemoteSchemaRelationshipsTable';
export { RemoteSchemaRelationshipTable } from './RemoteSchemaRelationshipsTable';
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 { Table } from '@/features/hasura-metadata-types';
import { RelationshipSourceType, RelationshipType } from './types';
import { isArray, isObject } from '@/components/Common/utils/jsUtils';
export const getRemoteRelationType = (
relation: RelationshipType
@ -62,10 +63,3 @@ 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

@ -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 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) => {
const values1 = Object.values(table1 as any).sort();
const values2 = Object.values(table2 as any).sort();
const values1 = isObjectOrArray(table1)
? Object.values(table1 as any).sort()
: table1;
const values2 = isObjectOrArray(table2)
? Object.values(table2 as any).sort()
: table2;
return isEqual(values1, values2);
};

View File

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

View File

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