console: support for remote relationship permission

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/10193
GitOrigin-RevId: ed90991c7919e50c51adcba323f57d6446e9679c
This commit is contained in:
Varun Choudhary 2023-09-01 12:11:35 +05:30 committed by hasura-bot
parent de8d130a47
commit 207dabec8d
10 changed files with 131 additions and 66 deletions

View File

@ -38,8 +38,8 @@ export const getTableDisplayName = (table: Table): string => {
return JSON.stringify(table);
};
export function isNotRemoteRelationship(
export function isNotRemoteSchemaRelationship(
relationship: Relationship
): relationship is LocalRelationship | RemoteDatabaseRelationship {
return relationship.relationshipType !== 'Remote';
return relationship.type !== 'remoteSchemaRelationship';
}

View File

@ -42,11 +42,13 @@ export const AggregationSection: React.FC<AggregationProps> = ({
setValue('aggregationEnabled', !enabled);
setValue(
'query_root_fields',
queryRootFields.filter((field: string) => field !== 'select_aggregate')
(queryRootFields ?? []).filter(
(field: string) => field !== 'select_aggregate'
)
);
setValue(
'subscription_root_fields',
subscriptionRootFields.filter(
(subscriptionRootFields ?? []).filter(
(field: string) => field !== 'select_aggregate'
)
);

View File

@ -1,7 +1,7 @@
import { Table } from '../../../../hasura-metadata-types';
import { useFormContext } from 'react-hook-form';
import { RowPermissionsInput } from './components';
import { RowPermissionsInput, TableToLoad } from './components';
import { usePermissionTables } from './hooks/usePermissionTables';
import { usePermissionComparators } from './hooks/usePermissionComparators';
import Skeleton from 'react-loading-skeleton';
@ -25,7 +25,9 @@ export const RowPermissionBuilder = ({
// this value will always be 'filter' or 'check' depending on the query type
const value = watch(permissionsKey);
const [tablesToLoad, setTablesToLoad] = useState<Table[]>([table]);
const [tablesToLoad, setTablesToLoad] = useState<TableToLoad>([
{ table, source: dataSourceName },
]);
const { tables, isLoading } = usePermissionTables({
dataSourceName,

View File

@ -95,7 +95,7 @@ export const Operator = ({
))}
</optgroup>
) : null}
{relationships?.length ? (
{relationships.length ? (
<optgroup label="Relationships">
{relationships.map((item, index) => (
<option

View File

@ -723,8 +723,11 @@ export const JsonbColumns: StoryObj<typeof RowPermissionsInput> = {
dataSourceName: 'default',
tablesToLoad: [
{
name: 'Stuff',
schema: 'public',
source: 'default',
table: {
name: 'Stuff',
schema: 'public',
},
},
],
});
@ -775,8 +778,11 @@ export const JsonbColumnsHasKeys: StoryObj<typeof RowPermissionsInput> = {
dataSourceName: 'default',
tablesToLoad: [
{
name: 'Stuff',
schema: 'public',
source: 'default',
table: {
name: 'Stuff',
schema: 'public',
},
},
],
});
@ -811,8 +817,11 @@ export const StringColumns: StoryObj<typeof RowPermissionsInput> = {
dataSourceName: 'default',
tablesToLoad: [
{
name: 'Stuff',
schema: 'public',
source: 'default',
table: {
name: 'Stuff',
schema: 'public',
},
},
],
});
@ -867,8 +876,11 @@ export const NumberColumns: StoryObj<typeof RowPermissionsInput> = {
dataSourceName: 'default',
tablesToLoad: [
{
name: 'Stuff',
schema: 'public',
source: 'default',
table: {
name: 'Stuff',
schema: 'public',
},
},
],
});
@ -1049,7 +1061,12 @@ export const RemoteRelationships: StoryObj<typeof RowPermissionsInput> = {
const [permissions, setPermissions] = useState<Permissions>({});
const { tables } = usePermissionTables({
dataSourceName: 'OhMy',
tablesToLoad: [['Chinook', 'Artist']],
tablesToLoad: [
{
source: 'Chinook',
table: ['Chinook', 'Artist'],
},
],
});
const comparators = usePermissionComparators();
@ -1093,7 +1110,12 @@ export const NestedObjects: StoryObj<typeof RowPermissionsInput> = {
render: args => {
const { tables } = usePermissionTables({
dataSourceName: 'M',
tablesToLoad: [['students']],
tablesToLoad: [
{
source: 'M',
table: ['students'],
},
],
});
const comparators = usePermissionComparators();
@ -1131,7 +1153,12 @@ export const NestedObjectsInitiallyEmpty: StoryObj<typeof RowPermissionsInput> =
render: args => {
const { tables } = usePermissionTables({
dataSourceName: 'M',
tablesToLoad: [['students']],
tablesToLoad: [
{
source: 'M',
table: ['students'],
},
],
});
const comparators = usePermissionComparators();
@ -1190,7 +1217,12 @@ export const NestedObjectsAnd: StoryObj<typeof RowPermissionsInput> = {
render: args => {
const { tables } = usePermissionTables({
dataSourceName: 'M',
tablesToLoad: [['students']],
tablesToLoad: [
{
source: 'M',
table: ['students'],
},
],
});
const comparators = usePermissionComparators();
@ -1227,7 +1259,12 @@ export const NestedObjectsOr: StoryObj<typeof RowPermissionsInput> = {
render: args => {
const { tables } = usePermissionTables({
dataSourceName: 'M',
tablesToLoad: [['students']],
tablesToLoad: [
{
source: 'M',
table: ['students'],
},
],
});
const comparators = usePermissionComparators();

View File

@ -30,23 +30,35 @@ export const TableProvider = ({
const [comparator, setComparator] = useState<string | undefined>();
const [columns, setColumns] = useState<Columns>([]);
const [relationships, setRelationships] = useState<Relationships>([]);
const { tables } = useContext(rootTableContext);
const { tables, rootTable } = useContext(rootTableContext);
const { loadRelationships } = useContext(rowPermissionsContext);
const { table: closestTableName } = useContext(tableContext);
const closestTable = tables.find(t =>
areTablesEqual(t.table, closestTableName)
);
const supportedSources = tables
.map(i => i.dataSource)
.filter(s => s?.kind === 'postgres')
.map(s => s?.name);
// Stringify values to get a stable value for useEffect
const stringifiedTable = JSON.stringify(table);
const stringifiedTables = JSON.stringify(tables);
useEffect(() => {
const foundTable = tables.find(t => areTablesEqual(t.table, table));
if (foundTable) {
setColumns(foundTable.columns);
if (foundTable?.dataSource?.name !== rootTable?.dataSource?.name) return;
setRelationships(
foundTable.relationships.filter(
relationship => relationship.type === 'localRelationship'
)
foundTable.relationships.filter(rel => {
return (
rel.type === 'localRelationship' ||
(rel.type !== 'remoteSchemaRelationship' &&
supportedSources?.includes(rel.definition.toSource))
);
})
);
// Load initial related tables' columns
loadRelationships?.(foundTable.relationships);

View File

@ -80,3 +80,5 @@ export type TableContext = {
relationships: Relationships;
setRelationships: (relationships: Relationships) => void;
};
export type TableToLoad = { source: string; table: Table }[];

View File

@ -1,20 +1,18 @@
import { getAllTableRelationships } from '../../../../../DatabaseRelationships/utils/tableRelationships';
import { useTablesWithColumns } from './useTablesWithColumns';
import { useSources } from '../../../../../MetadataAPI';
import { Tables } from '../components';
import { TableToLoad, Tables } from '../components';
import { useAllSuggestedRelationships } from '../../../../../DatabaseRelationships/components/SuggestedRelationships/hooks/useAllSuggestedRelationships';
import { Table } from '../../../../../hasura-metadata-types';
export const usePermissionTables = ({
dataSourceName,
tablesToLoad,
}: {
dataSourceName: string;
tablesToLoad: Table[];
tablesToLoad: TableToLoad;
}): { isLoading: boolean; tables: Tables | null } => {
const { data: sources, isLoading: isLoadingSources } = useSources();
const { data: tables, isLoading: isLoadingTables } = useTablesWithColumns({
dataSourceName,
tablesToLoad,
});
@ -31,10 +29,10 @@ export const usePermissionTables = ({
return {
isLoading: false,
tables:
tables?.map(({ metadataTable, columns }) => {
tables?.map(({ metadataTable, columns, sourceName }) => {
return {
table: metadataTable.table,
dataSource: sources?.find(source => source.name === dataSourceName),
dataSource: sources?.find(source => source.name === sourceName),
relationships: getAllTableRelationships(
metadataTable,
dataSourceName,

View File

@ -4,30 +4,25 @@ import {
exportMetadata,
TableColumn,
} from '../../../../../DataSource';
import { MetadataTable, Table } from '../../../../../hasura-metadata-types';
import { MetadataTable } from '../../../../../hasura-metadata-types';
import { useHttpClient } from '../../../../../Network';
import { areTablesEqual } from '../../../../../hasura-metadata-api';
const tablesQueryKey = (dataSourceName: string) => [
'tables-with-columns',
dataSourceName,
];
import { TableToLoad } from '../components';
export type TableWithColumns = {
metadataTable: MetadataTable;
columns: TableColumn[];
sourceName: string;
};
export const useTablesWithColumns = ({
dataSourceName,
tablesToLoad,
}: {
dataSourceName: string;
tablesToLoad: Table[];
tablesToLoad: TableToLoad;
}) => {
const httpClient = useHttpClient();
return useQuery<TableWithColumns[], Error>({
queryKey: [...tablesQueryKey(dataSourceName), tablesToLoad],
queryKey: [tablesToLoad],
queryFn: async () => {
const { metadata } = await exportMetadata({
httpClient,
@ -35,24 +30,29 @@ export const useTablesWithColumns = ({
if (!metadata) throw Error('metadata not found');
const currentMetadataSource = metadata.sources?.find(
source => source.name === dataSourceName
);
if (!currentMetadataSource)
throw Error(`useTables.metadataSource not found`);
const result: TableWithColumns[] = [];
for (const metadataTable of currentMetadataSource.tables) {
if (tablesToLoad.find(t => areTablesEqual(metadataTable.table, t))) {
const columns = await DataSource(httpClient).getTableColumns({
dataSourceName,
table: metadataTable.table,
});
result.push({ metadataTable, columns });
} else {
result.push({ metadataTable, columns: [] });
for (const source of metadata.sources) {
for (const metadataTable of source.tables) {
if (
tablesToLoad.find(
t =>
areTablesEqual(metadataTable.table, t.table) &&
source.name === t?.source
)
) {
const columns = await DataSource(httpClient).getTableColumns({
dataSourceName: source.name,
table: metadataTable.table,
});
result.push({ metadataTable, columns, sourceName: source.name });
} else {
result.push({
metadataTable,
columns: [],
sourceName: source.name,
});
}
}
}

View File

@ -1,6 +1,5 @@
import { isNotRemoteRelationship } from '../../../../../DatabaseRelationships/utils/helpers';
import { Table } from '../../../../../hasura-metadata-types';
import { Relationships } from '../components';
import { isNotRemoteSchemaRelationship } from '../../../../../DatabaseRelationships/utils/helpers';
import { Relationships, TableToLoad } from '../components';
/**
* Get the relationship tables that are not already in the tablesToLoad array
@ -10,9 +9,9 @@ export function getNewTablesToLoad({
tablesToLoad,
}: {
relationships: Relationships;
tablesToLoad: Table[];
}): Table[] {
const relatedTables = relationships.filter(isNotRemoteRelationship);
tablesToLoad: TableToLoad;
}): TableToLoad {
const relatedTables = relationships.filter(isNotRemoteSchemaRelationship);
// Create a Set of existing tables for faster lookups
const existingTablesSet = new Set<string>(
@ -23,11 +22,24 @@ export function getNewTablesToLoad({
const tablesToAddSet = new Set<string>();
relatedTables.forEach(relationship => {
const toTableString = JSON.stringify(relationship.definition.toTable);
let tableObject = undefined;
if (
'toSource' in relationship.definition &&
typeof relationship.definition.toTable === 'object'
) {
tableObject = JSON.stringify({
source: relationship.definition.toSource,
table: relationship.definition.toTable,
});
} else {
tableObject = JSON.stringify({
table: relationship.definition.toTable,
source: relationship.fromSource,
});
}
// Check if the toTable is not in the existing tables
if (!existingTablesSet.has(toTableString)) {
tablesToAddSet.add(toTableString);
if (!existingTablesSet.has(tableObject)) {
tablesToAddSet.add(tableObject);
}
});