mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-15 01:12:56 +03:00
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:
parent
01db431943
commit
2b4acb11e7
@ -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[];
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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">
|
||||
|
@ -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
|
||||
}
|
||||
>
|
||||
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
@ -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,
|
||||
|
@ -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';
|
||||
|
@ -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';
|
||||
|
||||
/**
|
||||
*
|
||||
|
@ -1,4 +1,3 @@
|
||||
export type { ExistingRelationshipMeta } from './RemoteSchemaRelationshipsTable';
|
||||
export { RemoteSchemaRelationshipTable } from './RemoteSchemaRelationshipsTable';
|
||||
export { getRemoteFieldPath } from './utils';
|
||||
export { areTablesEqual } from './utils';
|
||||
|
@ -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);
|
||||
};
|
||||
|
@ -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);
|
||||
}
|
||||
);
|
||||
});
|
@ -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);
|
||||
};
|
||||
|
@ -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();
|
||||
|
@ -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];
|
||||
|
Loading…
Reference in New Issue
Block a user