Allow user to view their table's foreign key info

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/7130
Co-authored-by: Julian <843342+okjulian@users.noreply.github.com>
GitOrigin-RevId: f2e015ff317edd71d628b58f8eaebad3cf2a3c69
This commit is contained in:
Julian@Hasura 2022-12-22 12:10:50 -03:00 committed by hasura-bot
parent bcb46c863d
commit 082f83fc20
13 changed files with 242 additions and 22 deletions

View File

@ -1,6 +1,11 @@
import { Table } from '@/features/hasura-metadata-types';
import React from 'react';
import { TableColumns, TableComments, TableRootFields } from './components';
import {
TableColumns,
TableComments,
TableRootFields,
ForeignKeys,
} from './components';
import { Section } from './parts';
export type ModifyTableProps = {
@ -18,6 +23,14 @@ export const ModifyTable: React.VFC<ModifyTableProps> = props => {
<Section headerText="Table Columns">
<TableColumns {...props} />
</Section>
<Section
headerText="Foreign Keys"
tooltipMessage={`
Foreign keys are one or more columns that point to another table's primary key. They link both tables.
`}
>
<ForeignKeys {...props} />
</Section>
<Section
headerText="Custom Field Names"
tooltipMessage="Customize table and column root names for GraphQL operations."

View File

@ -0,0 +1,19 @@
import React from 'react';
import {
TableFkRelationships,
generateForeignKeyLabel,
} from '@/features/DataSource';
export function ForeignKeyDescription({
foreignKey,
}: {
foreignKey: TableFkRelationships;
}) {
const label = generateForeignKeyLabel(foreignKey);
return (
<div key={label} className="mb-2">
{label}
</div>
);
}

View File

@ -0,0 +1,38 @@
import React from 'react';
import { IndicatorCard } from '@/new-components/IndicatorCard';
import Skeleton from 'react-loading-skeleton';
import { ModifyTableProps } from '../ModifyTable';
import { ForeignKeyDescription } from './ForeignKeyDescription';
import { useTableForeignKeys } from '../hooks/useTableForeignKeys';
interface ForeignKeysProps extends ModifyTableProps {}
export const ForeignKeys: React.VFC<ForeignKeysProps> = props => {
const { dataSourceName, table } = props;
const { data, isLoading, isError } = useTableForeignKeys({
dataSourceName,
table,
});
const foreignKeys = data?.foreignKeys ?? [];
if (isLoading || !foreignKeys) return <Skeleton count={5} height={20} />;
if (isError)
return (
<IndicatorCard status="negative" headline="error">
Unable to fetch foreign keys
</IndicatorCard>
);
if (!foreignKeys.length) {
// Some databases don't support foreign keys
return null;
}
return (
<>
{foreignKeys.map(foreignKey => (
<ForeignKeyDescription foreignKey={foreignKey} />
))}
</>
);
};

View File

@ -2,3 +2,4 @@ export { TableColumnDescription } from './TableColumnDescription';
export { TableColumns } from './TableColumns';
export { TableComments } from './TableComments';
export { TableRootFields } from './TableRootFields';
export { ForeignKeys } from './ForeignKeys';

View File

@ -0,0 +1,38 @@
import { DataSource, Feature } from '@/features/DataSource';
import { Table } from '@/features/hasura-metadata-types';
import { useHttpClient } from '@/features/Network';
import { useQuery } from 'react-query';
export const useTableForeignKeys = ({
table,
dataSourceName,
}: {
table: Table;
dataSourceName: string;
}) => {
const httpClient = useHttpClient();
return useQuery({
queryKey: ['foreign-keys-introspection', dataSourceName, table],
queryFn: async () => {
const foreignKeys = await DataSource(httpClient).getTableFkRelationships({
dataSourceName,
table,
});
const supportedOperators = await DataSource(
httpClient
).getSupportedOperators({
dataSourceName,
});
return {
foreignKeys,
supportedOperators:
supportedOperators === Feature.NotImplemented
? []
: supportedOperators,
};
},
refetchOnWindowFocus: false,
});
};

View File

@ -0,0 +1,48 @@
import { generateForeignKeyLabel } from '../utils';
describe('generateForeignKeyLabel', () => {
it('should accept arrays in target table', () => {
expect(
generateForeignKeyLabel({
from: { column: ['ArtistId'], table: ['Album'] },
to: { column: ['ArtistId'], table: ['Artist'] },
})
).toBe('ArtistId → Artist.ArtistId');
});
it('should accept strings in target table', () => {
expect(
generateForeignKeyLabel({
from: { column: ['ArtistId'], table: ['Album'] },
to: { column: ['ArtistId'], table: 'Artist' },
})
).toBe('ArtistId → Artist.ArtistId');
});
it('should separate nested tables with dots', () => {
expect(
generateForeignKeyLabel({
from: { column: ['ArtistId'], table: ['Album'] },
to: { column: ['ArtistId'], table: ['public', 'Artist'] },
})
).toBe('ArtistId → public.Artist.ArtistId');
});
it('should separate nested columns with commas', () => {
expect(
generateForeignKeyLabel({
from: { column: ['ArtistId'], table: ['Album'] },
to: { column: ['ArtistId', 'AuthorId'], table: ['Artist'] },
})
).toBe('ArtistId → Artist.ArtistId,AuthorId');
});
it('should remove double quotes from columns', () => {
expect(
generateForeignKeyLabel({
from: { column: ['"ArtistId"'], table: ['Album'] },
to: { column: ['"ArtistId"'], table: ['Artist'] },
})
).toBe('ArtistId → Artist.ArtistId');
});
});

View File

@ -1,7 +1,12 @@
import { FaFolder, FaTable } from 'react-icons/fa';
import React from 'react';
import { MetadataTable, Source, Table } from '@/features/hasura-metadata-types';
import { IntrospectedTable, TableColumn, TableRow } from '../types';
import {
IntrospectedTable,
TableColumn,
TableFkRelationships,
TableRow,
} from '../types';
import { RunSQLResponse } from '../api';
export const adaptIntrospectedTables = (
@ -112,3 +117,17 @@ export const transformGraphqlResponse = ({
return transformedRow;
});
};
export function generateForeignKeyLabel(foreignKey: TableFkRelationships) {
// foreignKey.to.table can be any valid Json value
// Handle the case where it is an array of strings, otherwise use it's stringified value
const toTableLabel = Array.isArray(foreignKey.to.table)
? foreignKey.to.table.join('.')
: foreignKey.to.table;
return `${foreignKey.from.column
.join(',')
// Replace double quotes with empty string
.replace(/"/g, '')} ${toTableLabel}.${foreignKey.to.column
.join(',')
.replace(/"/g, '')}`;
}

View File

@ -5,6 +5,7 @@ import {
getTableColumns,
getDatabaseConfiguration,
getSupportedOperators,
getFKRelationships,
} from './introspection';
import { getTableRows } from './query';
@ -27,7 +28,7 @@ export const gdc: Database = {
return Feature.NotImplemented;
},
getTableColumns,
getFKRelationships: async () => Feature.NotImplemented,
getFKRelationships,
getTablesListAsTree,
getSupportedOperators,
},

View File

@ -0,0 +1,39 @@
/* eslint-disable no-underscore-dangle */
import { runMetadataQuery } from '../../api';
import { GetFKRelationshipProps, TableFkRelationships } from '../../types';
import { GetTableInfoResponse } from './types';
export const getFKRelationships: (
props: GetFKRelationshipProps
) => Promise<TableFkRelationships[]> = async props => {
const { httpClient, dataSourceName, table } = props;
try {
const tableInfo = await runMetadataQuery<GetTableInfoResponse>({
httpClient,
body: {
type: 'get_table_info',
args: {
source: dataSourceName,
table,
},
},
});
if (!tableInfo.foreign_keys) {
return [];
}
return Object.entries(tableInfo.foreign_keys).map(([, foreignKey]) => {
const fromColumns = Object.keys(foreignKey.column_mapping);
const toColumns = Object.values(foreignKey.column_mapping);
return {
from: { column: fromColumns, table: props.table },
to: { column: toColumns, table: foreignKey.foreign_table },
};
});
} catch (error) {
console.error(error);
throw new Error('Error fetching GDC foreign keys');
}
};

View File

@ -9,24 +9,7 @@ import {
runMetadataQuery,
} from '../../api';
import { GetTableColumnsProps, TableColumn } from '../../types';
/**
* Refer - https://github.com/hasura/graphql-engine-mono/blob/main/dc-agents/dc-api-types/src/models/TableInfo.ts
*/
export type GetTableInfoResponse = {
name: GDCTable;
columns: { name: string; type: string; nullable: boolean }[];
primary_key?: string[] | null;
description?: string;
foreign_keys?: Record<
string,
{
foreign_table: GDCTable;
column_mapping: Record<string, string>;
}
>;
};
import { GetTableInfoResponse } from './types';
export const getTableColumns = async (props: GetTableColumnsProps) => {
const { httpClient, dataSourceName, table } = props;

View File

@ -1,7 +1,8 @@
export { getDatabaseConfiguration } from './getDatabaseConfiguration';
export { getSupportedOperators } from './getSupportedOperators';
export { getTableColumns } from './getTableColumns';
export { getFKRelationships } from './getFKRelationships';
export { getTablesListAsTree } from './getTablesListAsTree';
export { getTrackableTables } from './getTrackableTables';
export { convertToTreeData } from './utils';
export type { GetTableInfoResponse } from './getTableColumns';
export type { GetTableInfoResponse } from './types';

View File

@ -0,0 +1,19 @@
/**
* Refer - https://github.com/hasura/graphql-engine-mono/blob/main/dc-agents/dc-api-types/src/models/TableInfo.ts
*/
import { GDCTable } from '..';
export type GetTableInfoResponse = {
name: GDCTable;
columns: { name: string; type: string; nullable: boolean }[];
primary_key?: string[] | null;
description?: string;
foreign_keys?: Record<
string,
{
foreign_table: GDCTable;
column_mapping: Record<string, string>;
}
>;
};

View File

@ -419,6 +419,7 @@ export const DataSource = (httpClient: AxiosInstance) => ({
export { GDCTable } from './gdc';
export * from './guards';
export * from './types';
export * from './common/utils';
export {
PostgresTable,
exportMetadata,