mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-15 01:12:56 +03:00
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:
parent
bcb46c863d
commit
082f83fc20
@ -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."
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
@ -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} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
@ -2,3 +2,4 @@ export { TableColumnDescription } from './TableColumnDescription';
|
||||
export { TableColumns } from './TableColumns';
|
||||
export { TableComments } from './TableComments';
|
||||
export { TableRootFields } from './TableRootFields';
|
||||
export { ForeignKeys } from './ForeignKeys';
|
||||
|
@ -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,
|
||||
});
|
||||
};
|
@ -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');
|
||||
});
|
||||
});
|
@ -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, '')}`;
|
||||
}
|
||||
|
@ -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,
|
||||
},
|
||||
|
@ -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');
|
||||
}
|
||||
};
|
@ -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;
|
||||
|
@ -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';
|
||||
|
19
console/src/features/DataSource/gdc/introspection/types.ts
Normal file
19
console/src/features/DataSource/gdc/introspection/types.ts
Normal 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>;
|
||||
}
|
||||
>;
|
||||
};
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user