console (refactor): common hook to create and manage table relationships

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/9264
GitOrigin-RevId: 0cdbaf4eefe0932bc72c852da3abf88be78de47c
This commit is contained in:
Vijay Prasanna 2023-06-19 17:20:35 +05:30 committed by hasura-bot
parent 51aca1639a
commit 530e01d458
13 changed files with 3151 additions and 175 deletions

View File

@ -15,7 +15,10 @@ import { SuggestedRelationshipTrackModal } from '../../../DatabaseRelationships/
import Skeleton from 'react-loading-skeleton';
import { useAllSuggestedRelationships } from '../../../DatabaseRelationships/components/SuggestedRelationships/hooks/useAllSuggestedRelationships';
import { useCheckRows } from '../../../DatabaseRelationships/hooks/useCheckRows';
import { useTrackedRelationships } from './hooks/useTrackedRelationships';
// import { useTrackedRelationships } from './hooks/useTrackedRelationships';
import { useCreateTableRelationships } from '../../../DatabaseRelationships/hooks/useCreateTableRelationships/useCreateTableRelationships';
import { hasuraToast } from '../../../../new-components/Toasts';
import { DisplayToastErrorMessage } from '../../components/DisplayErrorMessage';
interface UntrackedRelationshipsProps {
dataSourceName: string;
@ -35,15 +38,18 @@ export const UntrackedRelationships: React.VFC<UntrackedRelationshipsProps> = ({
const {
suggestedRelationships,
isLoadingSuggestedRelationships,
onAddMultipleSuggestedRelationships,
// onAddMultipleSuggestedRelationships,
} = useAllSuggestedRelationships({
dataSourceName,
isEnabled: true,
omitTracked: true,
});
const { data: trackedRelationships } =
useTrackedRelationships(dataSourceName);
const { createTableRelationships, isLoading } =
useCreateTableRelationships(dataSourceName);
// const { data: trackedRelationships } =
// useTrackedRelationships(dataSourceName);
const checkboxRef = React.useRef<HTMLInputElement>(null);
const { checkedIds, onCheck, allChecked, toggleAll, reset, inputStatus } =
@ -54,8 +60,8 @@ export const UntrackedRelationships: React.VFC<UntrackedRelationshipsProps> = ({
const [filteredRelationships, setFilteredRelationships] = useState<
SuggestedRelationshipWithName[]
>(suggestedRelationships);
console.log('trackedRelationships', trackedRelationships);
console.log('suggestedRelationships', suggestedRelationships);
// console.log('trackedRelationships', trackedRelationships);
// console.log('suggestedRelationships', suggestedRelationships);
const serializedRelationshipNames = suggestedRelationships
.map(rel => rel.constraintName)
.join('-');
@ -78,27 +84,76 @@ export const UntrackedRelationships: React.VFC<UntrackedRelationshipsProps> = ({
checkboxRef.current.indeterminate = inputStatus === 'indeterminate';
}, [inputStatus]);
const onTrackRelationship = async (
relationship: SuggestedRelationshipWithName
) => {
await onAddMultipleSuggestedRelationships([relationship]);
const onTrackRelationship = async (rel: SuggestedRelationshipWithName) => {
await createTableRelationships({
data: [
{
name: rel.constraintName,
source: {
fromSource: dataSourceName,
fromTable: rel.from.table,
},
definition: {
target: {
toSource: dataSourceName,
toTable: rel.to.table,
},
type: rel.type,
detail: {
fkConstraintOn:
'constraint_name' in rel.from ? 'fromTable' : 'toTable',
fromColumns: rel.from.columns,
toColumns: rel.to.columns,
},
},
},
],
});
};
const [isTrackingSelectedRelationships, setTrackingSelectedRelationships] =
useState(false);
const onTrackSelected = async () => {
setTrackingSelectedRelationships(true);
try {
const selectedRelationships = suggestedRelationships.filter(rel =>
checkedIds.includes(rel.constraintName)
);
await onAddMultipleSuggestedRelationships(selectedRelationships);
} catch (err) {
setTrackingSelectedRelationships(false);
}
createTableRelationships({
data: selectedRelationships.map(rel => ({
name: rel.constraintName,
source: {
fromSource: dataSourceName,
fromTable: rel.from.table,
},
definition: {
target: {
toSource: dataSourceName,
toTable: rel.to.table,
},
type: rel.type,
detail: {
fkConstraintOn:
'constraint_name' in rel.from ? 'fromTable' : 'toTable',
fromColumns: rel.from.columns,
toColumns: rel.to.columns,
},
},
})),
onSettled: () => {
reset();
setTrackingSelectedRelationships(false);
},
onSuccess: () => {
hasuraToast({
type: 'success',
title: 'Successfully tracked relationships',
});
},
onError: err => {
hasuraToast({
type: 'error',
title: 'Error while tracking relationships',
children: <DisplayToastErrorMessage message={err.message} />,
});
},
});
};
if (isLoadingSuggestedRelationships) {
@ -125,7 +180,7 @@ export const UntrackedRelationships: React.VFC<UntrackedRelationshipsProps> = ({
mode="primary"
disabled={!checkedIds.length}
onClick={onTrackSelected}
isLoading={isTrackingSelectedRelationships}
isLoading={isLoading}
loadingText="Please Wait"
>
Track Selected ({checkedIds.length})

View File

@ -0,0 +1,49 @@
import { UseQueryOptions, useQuery } from 'react-query';
import { APIError } from '../../../hooks/error';
import { DataSource, Feature } from '../../DataSource';
import { Capabilities } from '@hasura/dc-api-types';
import { useHttpClient } from '../../Network';
import { useMetadata } from '../../hasura-metadata-api';
type AllCapabilitiesReturnType = {
driver: string;
capabilities: Feature | Capabilities;
}[];
export const useAllDriverCapabilities = <
FinalResult = AllCapabilitiesReturnType
>({
select,
options = {},
}: {
select?: (data: AllCapabilitiesReturnType) => FinalResult;
options?: UseQueryOptions<AllCapabilitiesReturnType, APIError, FinalResult>;
}) => {
const httpClient = useHttpClient();
const { data: driverNames = [], isFetching: isFetchingMetadata } =
useMetadata(m => m.metadata.sources.map(source => source.kind));
return useQuery<AllCapabilitiesReturnType, APIError, FinalResult>(
[driverNames, 'all_capabilities'],
async () => {
const result = driverNames.map(async driverName => {
const capabilities =
(await DataSource(httpClient).getDriverCapabilities(
driverName ?? ''
)) ?? Feature.NotImplemented;
return { driver: driverName, capabilities };
});
const finalResult = await Promise.all(result);
return finalResult;
},
{
enabled: !isFetchingMetadata && options.enabled,
select,
...options,
}
);
};

View File

@ -11,8 +11,7 @@ import {
getSupportedOperators,
} from './introspection';
import { getTableRows } from './query';
import { Capabilities } from '@hasura/dc-api-types';
import { DataTypeToSQLTypeMap } from './utils';
import { DataTypeToSQLTypeMap, bigQueryCapabilities } from './utils';
export type BigQueryTable = { name: string; dataset: string };
@ -29,9 +28,6 @@ export const bigquery: Database = {
return Feature.NotImplemented;
},
getDriverCapabilities: async () => {
const bigQueryCapabilities: Capabilities = {
queries: {},
};
return Promise.resolve(bigQueryCapabilities);
},
getTrackableTables,

View File

@ -1,3 +1,4 @@
import { Capabilities } from '@hasura/dc-api-types';
import { TableColumn } from '../types';
export const DataTypeToSQLTypeMap: Record<
@ -18,3 +19,13 @@ export const DataTypeToSQLTypeMap: Record<
json: [],
float: [],
};
export const bigQueryCapabilities: Capabilities = {
queries: {
foreach: {},
},
relationships: {},
data_schema: {
supports_foreign_keys: false,
},
};

View File

@ -6,7 +6,10 @@ export const postgresCapabilities: Capabilities = {
update: {},
delete: {},
},
queries: {},
queries: {
foreach: {},
},
relationships: {},
user_defined_functions: {},
data_schema: {
supports_foreign_keys: true,

View File

@ -0,0 +1,110 @@
import axios from 'axios';
import { DataSource } from '..';
describe('Verify capabilities for native drivers', () => {
it('[POSTGRES]', async () => {
const httpClient = axios.create();
const result = await DataSource(httpClient).getDriverCapabilities(
'postgres'
);
expect(result).toMatchInlineSnapshot(`
{
"data_schema": {
"supports_foreign_keys": true,
},
"mutations": {
"delete": {},
"insert": {},
"update": {},
},
"queries": {
"foreach": {},
},
"relationships": {},
"user_defined_functions": {},
}
`);
});
it('[COCKROACH]', async () => {
const httpClient = axios.create();
const result = await DataSource(httpClient).getDriverCapabilities(
'cockroach'
);
expect(result).toMatchInlineSnapshot(`
{
"data_schema": {
"supports_foreign_keys": true,
},
"mutations": {
"delete": {},
"insert": {},
"update": {},
},
"queries": {
"foreach": {},
},
"relationships": {},
"user_defined_functions": {},
}
`);
});
it('[CITUS]', async () => {
const httpClient = axios.create();
const result = await DataSource(httpClient).getDriverCapabilities('citus');
expect(result).toMatchInlineSnapshot(`
{
"data_schema": {
"supports_foreign_keys": true,
},
"mutations": {
"delete": {},
"insert": {},
"update": {},
},
"queries": {
"foreach": {},
},
"relationships": {},
"user_defined_functions": {},
}
`);
});
it('[MSSQL]', async () => {
const httpClient = axios.create();
const result = await DataSource(httpClient).getDriverCapabilities('mssql');
expect(result).toMatchInlineSnapshot(`
{
"data_schema": {
"supports_foreign_keys": true,
},
"mutations": {
"delete": {},
"insert": {},
"update": {},
},
"queries": {
"foreach": {},
},
"relationships": {},
"user_defined_functions": {},
}
`);
});
it('[BIGQUERY]', async () => {
const httpClient = axios.create();
const result = await DataSource(httpClient).getDriverCapabilities(
'bigquery'
);
expect(result).toMatchInlineSnapshot(`
{
"data_schema": {
"supports_foreign_keys": false,
},
"queries": {
"foreach": {},
},
"relationships": {},
}
`);
});
});

View File

@ -5,19 +5,10 @@ import {
} from '../../../DataSource/utils';
import { Table } from '../../../hasura-metadata-types';
import { useCallback } from 'react';
import { useManageLocalRelationship } from '../../hooks/useManageLocalRelationship';
import { useManageRemoteDatabaseRelationship } from '../../hooks/useManageRemoteDatabaseRelationship';
import { useManageRemoteSchemaRelationship } from '../../hooks/useManageRemoteSchemaRelationship';
import {
LocalRelationship,
MODE,
RemoteDatabaseRelationship,
RemoteSchemaRelationship,
} from '../../types';
import { MODE } from '../../types';
import { generateLhsFields } from './parts/MapRemoteSchemaFields/utils';
import { Schema } from './schema';
import { useDriverRelationshipSupport } from '../../../Data/hooks/useDriverRelationshipSupport';
import { hasuraToast } from '../../../../new-components/Toasts';
import { useCreateTableRelationships } from '../../hooks/useCreateTableRelationships/useCreateTableRelationships';
export const getTableLabel = ({
dataSourceName,
@ -54,141 +45,123 @@ export const useHandleSubmit = ({
onSuccess?: () => void;
onError?: (err: Error) => void;
}) => {
const { createRelationship: createLocalRelationship, isLoading } =
useManageLocalRelationship({
const { createTableRelationships, ...rest } = useCreateTableRelationships(
dataSourceName,
table,
{
onSuccess,
onError,
});
}
);
const {
createRelationship: createRemoteDatabaseRelationship,
editRelationship: editRemoteDatabaseRelationship,
isLoading: remoteDatabaseRelationshipLoading,
} = useManageRemoteDatabaseRelationship({
dataSourceName,
onSuccess,
onError,
});
const {
createRelationship: createRemoteSchemaRelationship,
editRelationship: editRemoteSchemaRelationship,
isLoading: remoteSchemaRelationshipLoading,
} = useManageRemoteSchemaRelationship({
dataSourceName,
onSuccess,
onError,
});
const { driverSupportsLocalRelationship, driverSupportsRemoteRelationship } =
useDriverRelationshipSupport({
dataSourceName,
});
const handleSubmit = useCallback(
(formData: Schema) => {
const { fromSource, toSource, details } = formData;
if (
!driverSupportsLocalRelationship &&
!driverSupportsRemoteRelationship
) {
hasuraToast({
type: 'error',
title: 'Not able to track',
message: `This datasource does not support tracking of relationships.`,
if (toSource.type === 'remoteSchema' && 'rsFieldMapping' in details) {
createTableRelationships({
data: [
{
name: formData.name,
source: {
fromSource: fromSource.dataSourceName,
fromTable: fromSource.table,
},
isEditMode: mode === MODE.EDIT,
definition: {
target: {
toRemoteSchema: toSource.remoteSchema,
},
detail: {
lhs_fields: generateLhsFields(details.rsFieldMapping),
remote_field: details.rsFieldMapping,
},
},
},
],
});
return;
}
// remote database relationship
/**
* Same database relationship
*/
if (
driverSupportsRemoteRelationship &&
toSource.type === 'table' &&
(!driverSupportsLocalRelationship || // if local relationship is not supported, we can create a remote relationship
toSource.dataSourceName !== dataSourceName) && // if local relationship is supported, we can create a remote relationship only if the table is from a different data source
fromSource.dataSourceName === toSource.dataSourceName &&
'columnMap' in details
) {
const remoteDatabaseRelationship: RemoteDatabaseRelationship = {
console.log('here', formData);
createTableRelationships({
data: [
{
name: formData.name,
type: 'remoteDatabaseRelationship',
source: {
fromSource: fromSource.dataSourceName,
fromTable: fromSource.table,
relationshipType: details.relationshipType,
},
isEditMode: mode === MODE.EDIT,
definition: {
target: {
toSource: toSource.dataSourceName,
toTable: toSource.table,
mapping: (details.columnMap ?? []).reduce(
},
type:
details.relationshipType === 'Object' ? 'object' : 'array',
detail: {
columnMapping: (details.columnMap ?? []).reduce(
(acc, entry) => ({ ...acc, [entry.from]: entry.to }),
{}
),
},
};
},
},
],
});
return;
}
if (mode === MODE.CREATE)
createRemoteDatabaseRelationship(remoteDatabaseRelationship);
else editRemoteDatabaseRelationship(remoteDatabaseRelationship);
} else if (
/**
* remote database relationship
*/
if (
toSource.type === 'table' &&
toSource.dataSourceName === dataSourceName &&
'columnMap' in details &&
driverSupportsLocalRelationship
fromSource.dataSourceName !== toSource.dataSourceName &&
'columnMap' in details
) {
const localRelationship: LocalRelationship = {
createTableRelationships({
data: [
{
name: formData.name,
type: 'localRelationship',
source: {
fromSource: fromSource.dataSourceName,
fromTable: fromSource.table,
relationshipType: details.relationshipType,
},
isEditMode: mode === MODE.EDIT,
definition: {
toTable: toSource.table,
mapping: (details.columnMap ?? []).reduce(
target: {
toRemoteSource: toSource.dataSourceName,
toRemoteTable: toSource.table,
},
type:
details.relationshipType === 'Object' ? 'object' : 'array',
detail: {
columnMapping: (details.columnMap ?? []).reduce(
(acc, entry) => ({ ...acc, [entry.from]: entry.to }),
{}
),
},
};
createLocalRelationship(localRelationship);
}
// remote schema relationship
if (toSource.type === 'remoteSchema' && 'rsFieldMapping' in details) {
const remoteSchemaRelationship: RemoteSchemaRelationship = {
name: formData.name,
type: 'remoteSchemaRelationship',
relationshipType: 'Remote',
fromSource: fromSource.dataSourceName,
fromTable: fromSource.table,
definition: {
toRemoteSchema: toSource.remoteSchema,
lhs_fields: generateLhsFields(details.rsFieldMapping),
remote_field: details.rsFieldMapping,
},
};
if (mode === MODE.CREATE)
createRemoteSchemaRelationship(remoteSchemaRelationship);
else editRemoteSchemaRelationship(remoteSchemaRelationship);
},
],
});
return;
}
},
[
createLocalRelationship,
createRemoteDatabaseRelationship,
createRemoteSchemaRelationship,
dataSourceName,
editRemoteDatabaseRelationship,
editRemoteSchemaRelationship,
mode,
driverSupportsLocalRelationship,
driverSupportsRemoteRelationship,
]
[createTableRelationships, mode]
);
return {
handleSubmit,
isLoading:
isLoading ||
remoteDatabaseRelationshipLoading ||
remoteSchemaRelationshipLoading,
...rest,
};
};

View File

@ -10,6 +10,8 @@ import {
SuggestedRelationshipWithName,
useSuggestedRelationships,
} from '../SuggestedRelationships/hooks/useSuggestedRelationships';
import { useCreateTableRelationships } from '../../hooks/useCreateTableRelationships/useCreateTableRelationships';
import { DisplayToastErrorMessage } from '../../../Data/components/DisplayErrorMessage';
type SuggestedRelationshipTrackModalProps = {
relationship: SuggestedRelationshipWithName;
@ -20,33 +22,58 @@ type SuggestedRelationshipTrackModalProps = {
export const SuggestedRelationshipTrackModal: React.VFC<
SuggestedRelationshipTrackModalProps
> = ({ relationship, dataSourceName, onClose }) => {
const {
onAddSuggestedRelationship,
isAddingSuggestedRelationship,
refetchSuggestedRelationships,
} = useSuggestedRelationships({
const { refetchSuggestedRelationships } = useSuggestedRelationships({
dataSourceName,
table: relationship.from.table,
existingRelationships: [],
isEnabled: true,
});
const onTrackRelationship = async (relationshipName: string) => {
try {
await onAddSuggestedRelationship({
...relationship,
constraintName: relationshipName,
});
const { createTableRelationships, isLoading } =
useCreateTableRelationships(dataSourceName);
const onTrackRelationship = async (relationshipName: string) => {
createTableRelationships({
data: [
{
name: relationshipName,
source: {
fromSource: dataSourceName,
fromTable: relationship.from.table,
},
definition: {
target: {
toSource: dataSourceName,
toTable: relationship.to.table,
},
type: relationship.type,
detail: {
fkConstraintOn:
'constraint_name' in relationship.from
? 'fromTable'
: 'toTable',
fromColumns: relationship.from.columns,
toColumns: relationship.to.columns,
},
},
},
],
onSuccess: () => {
hasuraToast({
type: 'success',
title: 'Tracked Successfully',
});
refetchSuggestedRelationships();
onClose();
} catch (err: unknown) {
},
onError: err => {
hasuraToast({
title: 'Error',
message: err instanceof Error ? err.message : 'An error occurred',
type: 'error',
title: 'Failed to track',
children: <DisplayToastErrorMessage message={err.message} />,
});
},
});
}
};
const { Form, methods } = useConsoleForm({
@ -85,7 +112,7 @@ export const SuggestedRelationshipTrackModal: React.VFC<
callToDeny="Cancel"
callToAction="Track relationship"
onClose={onClose}
isLoading={isAddingSuggestedRelationship}
isLoading={isLoading}
/>
</>
</Form>

View File

@ -0,0 +1,24 @@
import {
AllowedRelationshipDefinitions,
LocalTableRelationshipDefinition,
RemoteSchemaRelationshipDefinition,
RemoteTableRelationshipDefinition,
} from './types';
export const isLocalTableRelationshipDefinition = (
value: AllowedRelationshipDefinitions
): value is LocalTableRelationshipDefinition => {
return 'toSource' in value.target;
};
export const isRemoteTableRelationshipDefinition = (
value: AllowedRelationshipDefinitions
): value is RemoteTableRelationshipDefinition => {
return 'toRemoteSource' in value.target;
};
export const isRemoteSchemaRelationshipDefinition = (
value: AllowedRelationshipDefinitions
): value is RemoteSchemaRelationshipDefinition => {
return 'toRemoteSchema' in value.target;
};

View File

@ -0,0 +1,74 @@
import { Table } from '../../../hasura-metadata-types';
export type TableRelationshipBasicDetails = {
driver: string;
name: string;
source: { fromSource: string; fromTable: Table };
isEditMode?: boolean;
};
export type LocalTableRelationshipDefinition = {
target: {
toSource: string;
toTable: Table;
};
type: 'array' | 'object';
detail:
| {
fkConstraintOn: 'fromTable' | 'toTable';
fromColumns: string[];
toColumns: string[];
}
| { columnMapping: Record<string, string> };
};
export type RemoteSchemaRelationshipDefinition = {
target: {
toRemoteSchema: string;
};
detail: { lhs_fields: string[]; remote_field: Record<string, any> };
};
export type RemoteTableRelationshipDefinition = {
target: {
toRemoteSource: string;
toRemoteTable: Table;
};
type: 'array' | 'object';
detail: { columnMapping: Record<string, string> };
};
export type AllowedRelationshipDefinitions =
| LocalTableRelationshipDefinition
| RemoteSchemaRelationshipDefinition
| RemoteTableRelationshipDefinition;
export type CreateTableRelationshipRequestBodyProps =
TableRelationshipBasicDetails & {
definition: AllowedRelationshipDefinitions;
targetCapabilities: {
isLocalTableRelationshipSupported: boolean;
isRemoteTableRelationshipSupported: boolean;
isRemoteSchemaRelationshipSupported: boolean;
};
sourceCapabilities: {
isLocalTableRelationshipSupported: boolean;
isRemoteTableRelationshipSupported: boolean;
isRemoteSchemaRelationshipSupported: boolean;
};
};
export type RenameRelationshipProps = {
name: string;
driver: string;
new_name: string;
source: string;
table: Table;
};
export type DeleteRelationshipProps = {
driver: string;
name: string;
table: Table;
source: string;
isRemote: boolean;
};

View File

@ -0,0 +1,267 @@
import { useCallback } from 'react';
import { isObject } from '../../../../components/Common/utils/jsUtils';
import { transformErrorResponse } from '../../../Data/errorUtils';
// import {
// useAllDriverCapabilities,
// useDriverCapabilities,
// } from '../../../Data/hooks/useDriverCapabilities';
import { Feature } from '../../../DataSource';
import { useMetadataMigration } from '../../../MetadataAPI';
import { MetadataMigrationOptions } from '../../../MetadataAPI/hooks/useMetadataMigration';
import {
areTablesEqual,
useInvalidateMetadata,
useMetadata,
} from '../../../hasura-metadata-api';
import {
createTableRelationshipRequestBody,
deleteTableRelationshipRequestBody,
renameRelationshipRequestBody,
} from './utils';
import {
DeleteRelationshipProps,
LocalTableRelationshipDefinition,
RemoteSchemaRelationshipDefinition,
RemoteTableRelationshipDefinition,
RenameRelationshipProps,
TableRelationshipBasicDetails,
} from './types';
import { Table } from '../../../hasura-metadata-types';
import { useAllDriverCapabilities } from '../../../Data/hooks/useAllDriverCapabilities';
type AllowedRelationshipDefinitions =
| Omit<LocalTableRelationshipDefinition, 'capabilities'>
| Omit<RemoteTableRelationshipDefinition, 'capabilities'>
| Omit<RemoteSchemaRelationshipDefinition, 'capabilities'>;
type CreateTableRelationshipProps = Omit<
TableRelationshipBasicDetails,
'driver'
> & {
definition: AllowedRelationshipDefinitions;
};
type RenameTableRelationshipProps = Omit<RenameRelationshipProps, 'driver'>;
type DeleteTableRelationshipProps = Omit<
DeleteRelationshipProps,
'driver' | 'isRemote'
>;
const defaultCapabilities = {
isLocalTableRelationshipSupported: false,
isRemoteTableRelationshipSupported: false,
isRemoteSchemaRelationshipSupported: true,
};
const getTargetName = (target: AllowedRelationshipDefinitions['target']) => {
if ('toRemoteSchema' in target) return null;
if ('toRemoteSource' in target) return target.toRemoteSource;
return target.toSource;
};
export const useCreateTableRelationships = (
dataSourceName: string,
globalMutateOptions?: MetadataMigrationOptions
) => {
const invalidateMetadata = useInvalidateMetadata();
// get these capabilities
const { data: driverCapabilties = [] } = useAllDriverCapabilities({
select: data => {
const result = data.map(item => {
if (item.capabilities === Feature.NotImplemented)
return {
driver: item.driver,
capabilities: {
isLocalTableRelationshipSupported: false,
isRemoteTableRelationshipSupported: false,
isRemoteSchemaRelationshipSupported: false,
},
};
return {
driver: item.driver,
capabilities: {
isLocalTableRelationshipSupported: isObject(
item.capabilities.relationships
),
isRemoteTableRelationshipSupported: isObject(
item.capabilities.queries?.foreach
),
isRemoteSchemaRelationshipSupported: true,
},
};
});
return result;
},
});
// const {
// data: capabilities = {
// isLocalTableRelationshipSupported: false,
// isRemoteTableRelationshipSupported: false,
// isRemoteSchemaRelationshipSupported: true,
// },
// } = useDriverCapabilities({
// dataSourceName,
// select: data => {
// if (data === Feature.NotImplemented)
// return {
// isLocalTableRelationshipSupported: false,
// isRemoteTableRelationshipSupported: false,
// isRemoteSchemaRelationshipSupported: false,
// };
// return {
// isLocalTableRelationshipSupported: isObject(data.relationships),
// isRemoteTableRelationshipSupported: isObject(data.queries?.foreach),
// isRemoteSchemaRelationshipSupported: true,
// };
// },
// });
// eslint-disable-next-line react-hooks/exhaustive-deps
const { data: { metadataSources = [], resource_version } = {} } = useMetadata(
m => ({
metadataSources: m.metadata.sources,
resource_version: m.resource_version,
})
);
const getDriver = useCallback(
(source: string) => metadataSources.find(s => s.name === source)?.kind,
[metadataSources]
);
const isRemoteRelationship = useCallback(
(source: string, table: Table, relName: string) => {
return !!metadataSources
.find(s => s.name === source)
?.tables.find(t => areTablesEqual(t.table, table))
?.remote_relationships?.find(r => r.name === relName);
},
[metadataSources]
);
const { mutate, ...rest } = useMetadataMigration({
...globalMutateOptions,
errorTransform: transformErrorResponse,
onSuccess: (data, variable, ctx) => {
invalidateMetadata();
globalMutateOptions?.onSuccess?.(data, variable, ctx);
},
});
const createTableRelationships = useCallback(
async ({
data,
...options
}: { data: CreateTableRelationshipProps[] } & MetadataMigrationOptions) => {
const payloads = data.map(item => {
return createTableRelationshipRequestBody({
driver: getDriver(item.source.fromSource) ?? '',
name: item.name,
source: {
fromSource: item.source.fromSource,
fromTable: item.source.fromTable,
},
definition: {
...item.definition,
},
sourceCapabilities:
driverCapabilties.find(
c => c.driver === getDriver(item.source.fromSource)
)?.capabilities ?? defaultCapabilities,
targetCapabilities:
driverCapabilties.find(
c =>
c.driver ===
getDriver(getTargetName(item.definition.target) ?? '')
)?.capabilities ?? defaultCapabilities,
});
});
mutate(
{
query: {
type: 'bulk_keep_going',
args: payloads,
resource_version,
},
},
options
);
},
[driverCapabilties, getDriver, mutate, resource_version]
);
const renameRelationships = useCallback(
async ({
data,
...options
}: { data: RenameTableRelationshipProps[] } & MetadataMigrationOptions) => {
const payloads = data.map(item => {
return renameRelationshipRequestBody({
driver: getDriver(item.source) ?? '',
name: item.name,
source: item.source,
new_name: item.new_name,
table: item.table,
});
});
mutate(
{
query: {
type: 'bulk_keep_going',
args: payloads,
resource_version,
},
},
options
);
},
[getDriver, mutate, resource_version]
);
const deleteRelationships = useCallback(
async ({
data,
...options
}: { data: DeleteTableRelationshipProps[] } & MetadataMigrationOptions) => {
const payloads = data.map(item => {
return deleteTableRelationshipRequestBody({
driver: getDriver(item.source) ?? '',
name: item.name,
source: item.source,
table: item.table,
isRemote: isRemoteRelationship(item.source, item.table, item.name),
});
});
mutate(
{
query: {
type: 'bulk_keep_going',
args: payloads,
resource_version,
},
},
options
);
},
[getDriver, isRemoteRelationship, mutate, resource_version]
);
return {
createTableRelationships,
renameRelationships,
deleteRelationships,
...rest,
};
};

View File

@ -0,0 +1,280 @@
import { Table } from '../../../hasura-metadata-types';
import zipObject from 'lodash/zipObject';
import {
CreateTableRelationshipRequestBodyProps,
DeleteRelationshipProps,
RenameRelationshipProps,
} from './types';
import {
isLocalTableRelationshipDefinition,
isRemoteSchemaRelationshipDefinition,
isRemoteTableRelationshipDefinition,
} from './typeGuards';
const createLocalFkRelationshipRequestBody = (props: {
name: string;
type: 'array' | 'object';
driver: string;
source: string;
fromTable: Table;
toTable: Table;
fkConstraintOn: 'fromTable' | 'toTable';
fromColumns: string[];
toColumns: string[];
}) => {
return {
type: `${props.driver}_create_${props.type}_relationship`,
args: {
table: props.fromTable,
name: props.name,
source: props.source,
using: {
foreign_key_constraint_on:
props.fkConstraintOn === 'fromTable'
? props.fromColumns
: {
table: props.toTable,
columns: props.toColumns,
},
},
},
};
};
export const renameRelationshipRequestBody = (
props: RenameRelationshipProps
) => ({
type: `${props.driver}_rename_relationship`,
args: {
table: props.table,
name: props.name,
source: props.source,
new_name: props.new_name,
},
});
const createLocalManualRelationship = (props: {
name: string;
type: 'array' | 'object';
driver: string;
source: string;
fromTable: Table;
toTable: Table;
columnMapping: Record<string, string>;
}) => {
return {
type: `${props.driver}_create_${props.type}_relationship`,
args: {
table: props.fromTable,
name: props.name,
source: props.source,
using: {
manual_configuration: {
remote_table: props.toTable,
column_mapping: props.columnMapping,
},
},
},
};
};
const createRemoteTableRelationshipRequestBody = (props: {
name: string;
type: 'array' | 'object';
driver: string;
fromSource: string;
toSource: string;
fromTable: Table;
toTable: Table;
columnMapping: Record<string, string>;
editMode?: boolean;
}) => {
return {
type: `${props.driver}_${
props.editMode ? 'update' : 'create'
}_remote_relationship`,
args: {
name: props.name,
source: props.fromSource,
table: props.fromTable,
definition: {
to_source: {
relationship_type: props.type,
source: props.toSource,
table: props.toTable,
field_mapping: props.columnMapping,
},
},
},
};
};
const createRemoteSchemaRelationshipRequestBody = (props: {
name: string;
driver: string;
fromSource: string;
fromTable: Table;
toRemoteSchema: string;
lhs_fields: string[];
remote_field: Record<string, any>;
editMode?: boolean;
}) => {
return {
type: `${props.driver}_${
props.editMode ? 'update' : 'create'
}_remote_relationship`,
args: {
name: props.name,
source: props.fromSource,
table: props.fromTable,
definition: {
to_remote_schema: {
remote_schema: props.toRemoteSchema,
lhs_fields: props.lhs_fields,
remote_field: props.remote_field,
},
},
},
};
};
export const createTableRelationshipRequestBody = (
props: CreateTableRelationshipRequestBodyProps
) => {
if (isLocalTableRelationshipDefinition(props.definition)) {
if (props.isEditMode) {
if (
!props.sourceCapabilities.isLocalTableRelationshipSupported &&
props.targetCapabilities.isRemoteTableRelationshipSupported
) {
return createRemoteTableRelationshipRequestBody({
name: props.name,
driver: props.driver,
type: props.definition.type,
fromSource: props.source.fromSource,
toSource: props.definition.target.toSource,
fromTable: props.source.fromTable,
toTable: props.definition.target.toTable,
columnMapping:
'fkConstraintOn' in props.definition.detail
? zipObject(
props.definition.detail.fromColumns,
props.definition.detail.toColumns
)
: props.definition.detail.columnMapping,
editMode: true,
});
}
throw Error('Edit Local Relationship not supported');
}
if (props.source.fromSource === props.definition.target.toSource) {
if (props.sourceCapabilities.isLocalTableRelationshipSupported) {
return 'fkConstraintOn' in props.definition.detail
? createLocalFkRelationshipRequestBody({
name: props.name,
driver: props.driver,
source: props.source.fromSource,
type: props.definition.type,
fromTable: props.source.fromTable,
toTable: props.definition.target.toTable,
fkConstraintOn: props.definition.detail.fkConstraintOn,
fromColumns: props.definition.detail.fromColumns,
toColumns: props.definition.detail.toColumns,
})
: createLocalManualRelationship({
name: props.name,
driver: props.driver,
source: props.source.fromSource,
type: props.definition.type,
fromTable: props.source.fromTable,
toTable: props.definition.target.toTable,
columnMapping: props.definition.detail.columnMapping,
});
}
if (
!props.sourceCapabilities.isLocalTableRelationshipSupported &&
props.targetCapabilities.isRemoteTableRelationshipSupported
) {
return createRemoteTableRelationshipRequestBody({
name: props.name,
driver: props.driver,
type: props.definition.type,
fromSource: props.source.fromSource,
toSource: props.definition.target.toSource,
fromTable: props.source.fromTable,
toTable: props.definition.target.toTable,
columnMapping:
'fkConstraintOn' in props.definition.detail
? zipObject(
props.definition.detail.fromColumns,
props.definition.detail.toColumns
)
: props.definition.detail.columnMapping,
});
}
throw Error('Local Relationship not supported');
}
}
if (isRemoteTableRelationshipDefinition(props.definition)) {
if (props.targetCapabilities.isRemoteTableRelationshipSupported) {
return createRemoteTableRelationshipRequestBody({
name: props.name,
driver: props.driver,
type: props.definition.type,
fromSource: props.source.fromSource,
toSource: props.definition.target.toRemoteSource,
fromTable: props.source.fromTable,
toTable: props.definition.target.toRemoteTable,
columnMapping: props.definition.detail.columnMapping,
editMode: props.isEditMode,
});
}
throw Error('Remote Database Relationship not supported');
}
if (isRemoteSchemaRelationshipDefinition(props.definition)) {
if (props.sourceCapabilities.isRemoteSchemaRelationshipSupported) {
return createRemoteSchemaRelationshipRequestBody({
name: props.name,
driver: props.driver,
fromSource: props.source.fromSource,
fromTable: props.source.fromTable,
toRemoteSchema: props.definition.target.toRemoteSchema,
lhs_fields: props.definition.detail.lhs_fields,
remote_field: props.definition.detail.remote_field,
editMode: props.isEditMode,
});
}
throw Error('Remote Schema Relationship not supported');
}
return 'Not implemented';
};
export const deleteTableRelationshipRequestBody = (
props: DeleteRelationshipProps
) => {
if (props.isRemote)
return {
type: `${props.driver}_delete_remote_relationship`,
args: {
source: props.source,
table: props.table,
name: props.name,
},
};
return {
type: `${props.driver}_drop_relationship`,
args: {
source: props.source,
table: props.table,
relationship: props.name,
},
};
};