console: add remote database relationships

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/3412
Co-authored-by: Vijay Prasanna <11921040+vijayprasanna13@users.noreply.github.com>
GitOrigin-RevId: 22052291dcc15e6a10ed573f5044537401d366fe
This commit is contained in:
Abhijeet Khangarot 2022-02-04 19:25:28 +05:30 committed by hasura-bot
parent 780d942fad
commit 4453745c57
27 changed files with 1222 additions and 22 deletions

View File

@ -19,6 +19,7 @@ or the `HASURA_GRAPHQL_OPTIMIZE_PERMISSION_FILTERS` environment variable.
### Bug fixes and improvements
(Add entries below in the order of server, console, cli, docs, others)
- console: add support for remote database relationships
- cli: skip tls verfication for all API requests when `insecure-skip-tls-verify` flag is set (#4926)
## v2.2.0

View File

@ -16778,6 +16778,11 @@
}
}
},
"base16": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/base16/-/base16-1.0.0.tgz",
"integrity": "sha1-4pf2DX7BAUp6lxo568ipjAtoHnA="
},
"base64-js": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz",
@ -18831,6 +18836,14 @@
}
}
},
"cross-fetch": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz",
"integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==",
"requires": {
"node-fetch": "2.6.7"
}
},
"cross-spawn": {
"version": "6.0.5",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
@ -21595,6 +21608,30 @@
"bser": "2.1.1"
}
},
"fbemitter": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/fbemitter/-/fbemitter-3.0.0.tgz",
"integrity": "sha512-KWKaceCwKQU0+HPoop6gn4eOHk50bBv/VxjJtGMfwmJt3D29JpN4H4eisCtIPA+a8GVBam+ldMMpMjJUvpDyHw==",
"requires": {
"fbjs": "^3.0.0"
},
"dependencies": {
"fbjs": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/fbjs/-/fbjs-3.0.2.tgz",
"integrity": "sha512-qv+boqYndjElAJHNN3NoM8XuwQZ1j2m3kEvTgdle8IDjr6oUbkEpvABWtj/rQl3vq4ew7dnElBxL4YJAwTVqQQ==",
"requires": {
"cross-fetch": "^3.0.4",
"fbjs-css-vars": "^1.0.0",
"loose-envify": "^1.0.0",
"object-assign": "^4.1.0",
"promise": "^7.1.1",
"setimmediate": "^1.0.5",
"ua-parser-js": "^0.7.30"
}
}
}
},
"fbjs": {
"version": "0.8.17",
"resolved": "https://registry.npmjs.org/fbjs/-/fbjs-0.8.17.tgz",
@ -21848,6 +21885,31 @@
"readable-stream": "^2.3.6"
}
},
"flux": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/flux/-/flux-4.0.3.tgz",
"integrity": "sha512-yKAbrp7JhZhj6uiT1FTuVMlIAT1J4jqEyBpFApi1kxpGZCvacMVc/t1pMQyotqHhAgvoE3bNvAykhCo2CLjnYw==",
"requires": {
"fbemitter": "^3.0.0",
"fbjs": "^3.0.1"
},
"dependencies": {
"fbjs": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/fbjs/-/fbjs-3.0.2.tgz",
"integrity": "sha512-qv+boqYndjElAJHNN3NoM8XuwQZ1j2m3kEvTgdle8IDjr6oUbkEpvABWtj/rQl3vq4ew7dnElBxL4YJAwTVqQQ==",
"requires": {
"cross-fetch": "^3.0.4",
"fbjs-css-vars": "^1.0.0",
"loose-envify": "^1.0.0",
"object-assign": "^4.1.0",
"promise": "^7.1.1",
"setimmediate": "^1.0.5",
"ua-parser-js": "^0.7.30"
}
}
}
},
"font-awesome": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/font-awesome/-/font-awesome-4.7.0.tgz",
@ -28029,6 +28091,11 @@
"integrity": "sha1-wCUTUV4wna3dTCTGDP3c9ZdtkRU=",
"dev": true
},
"lodash.curry": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.curry/-/lodash.curry-4.1.1.tgz",
"integrity": "sha1-JI42By7ekGUB11lmIAqG2riyMXA="
},
"lodash.debounce": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
@ -28046,6 +28113,11 @@
"integrity": "sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=",
"dev": true
},
"lodash.flow": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/lodash.flow/-/lodash.flow-3.5.0.tgz",
"integrity": "sha1-h79AKSuM+D5OjOGjrkIJ4gBxZ1o="
},
"lodash.get": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
@ -29357,7 +29429,6 @@
"version": "2.6.7",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
"integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
"dev": true,
"requires": {
"whatwg-url": "^5.0.0"
},
@ -29365,20 +29436,17 @@
"tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=",
"dev": true
"integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o="
},
"webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=",
"dev": true
"integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE="
},
"whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=",
"dev": true,
"requires": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
@ -32637,6 +32705,11 @@
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A=="
},
"pure-color": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/pure-color/-/pure-color-1.3.0.tgz",
"integrity": "sha1-H+Bk+wrIUfDeYTIKi/eWg2Qi8z4="
},
"purgecss": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/purgecss/-/purgecss-4.0.3.tgz",
@ -32875,6 +32948,17 @@
"section-iterator": "^2.0.0"
}
},
"react-base16-styling": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/react-base16-styling/-/react-base16-styling-0.6.0.tgz",
"integrity": "sha1-7yFW1mz0E5aVyKFniGy2nqZgeSw=",
"requires": {
"base16": "^1.0.0",
"lodash.curry": "^4.0.1",
"lodash.flow": "^3.3.0",
"pure-color": "^1.2.0"
}
},
"react-bootstrap": {
"version": "0.32.4",
"resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-0.32.4.tgz",
@ -33329,6 +33413,17 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.12.0.tgz",
"integrity": "sha512-rPCkf/mWBtKc97aLL9/txD8DZdemK0vkA3JMLShjlJB3Pj3s+lpf1KaBzMfQrAmhMQB0n1cU/SUGgKKBCe837Q=="
},
"react-json-view": {
"version": "1.21.3",
"resolved": "https://registry.npmjs.org/react-json-view/-/react-json-view-1.21.3.tgz",
"integrity": "sha512-13p8IREj9/x/Ye4WI/JpjhoIwuzEgUAtgJZNBJckfzJt1qyh24BdTm6UQNGnyTq9dapQdrqvquZTo3dz1X6Cjw==",
"requires": {
"flux": "^4.0.1",
"react-base16-styling": "^0.6.0",
"react-lifecycles-compat": "^3.0.4",
"react-textarea-autosize": "^8.3.2"
}
},
"react-lifecycles-compat": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
@ -33622,7 +33717,6 @@
"version": "8.3.3",
"resolved": "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-8.3.3.tgz",
"integrity": "sha512-2XlHXK2TDxS6vbQaoPbMOfQ8GK7+irc2fVK6QFIcC8GOnH3zI/v481n+j1L0WaPVvKxwesnY93fEfH++sus2rQ==",
"dev": true,
"requires": {
"@babel/runtime": "^7.10.2",
"use-composed-ref": "^1.0.0",
@ -38225,7 +38319,6 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/use-composed-ref/-/use-composed-ref-1.1.0.tgz",
"integrity": "sha512-my1lNHGWsSDAhhVAT4MKs6IjBUtG6ZG11uUqexPH9PptiIZDQOzaF4f5tEbJ2+7qvNbtXNBbU3SfmN+fXlWDhg==",
"dev": true,
"requires": {
"ts-essentials": "^2.0.3"
},
@ -38233,22 +38326,19 @@
"ts-essentials": {
"version": "2.0.12",
"resolved": "https://registry.npmjs.org/ts-essentials/-/ts-essentials-2.0.12.tgz",
"integrity": "sha512-3IVX4nI6B5cc31/GFFE+i8ey/N2eA0CZDbo6n0yrz0zDX8ZJ8djmU1p+XRz7G3is0F3bB3pu2pAroFdAWQKU3w==",
"dev": true
"integrity": "sha512-3IVX4nI6B5cc31/GFFE+i8ey/N2eA0CZDbo6n0yrz0zDX8ZJ8djmU1p+XRz7G3is0F3bB3pu2pAroFdAWQKU3w=="
}
}
},
"use-isomorphic-layout-effect": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.1.tgz",
"integrity": "sha512-L7Evj8FGcwo/wpbv/qvSfrkHFtOpCzvM5yl2KVyDJoylVuSvzphiiasmjgQPttIGBAy2WKiBNR98q8w7PiNgKQ==",
"dev": true
"integrity": "sha512-L7Evj8FGcwo/wpbv/qvSfrkHFtOpCzvM5yl2KVyDJoylVuSvzphiiasmjgQPttIGBAy2WKiBNR98q8w7PiNgKQ=="
},
"use-latest": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/use-latest/-/use-latest-1.2.0.tgz",
"integrity": "sha512-d2TEuG6nSLKQLAfW3By8mKr8HurOlTkul0sOpxbClIv4SQ4iOd7BYr7VIzdbktUCnv7dua/60xzd8igMU6jmyw==",
"dev": true,
"requires": {
"use-isomorphic-layout-effect": "^1.0.0"
}

View File

@ -98,6 +98,7 @@
"react-helmet": "5.2.1",
"react-hook-form": "7.15.4",
"react-icons": "^4.3.1",
"react-json-view": "^1.21.3",
"react-modal": "3.11.2",
"react-notification-system-redux": "2.0.1",
"react-progress-bar-plus": "1.3.1",

View File

@ -17,6 +17,8 @@ import {
getCreateArrayRelationshipQuery,
getDropRelationshipQuery,
getAddRelationshipQuery,
getSaveRemoteDbRelationshipQuery,
getDropRemoteDbRelationshipQuery,
} from '../../../../metadata/queryUtils';
import Migration from '../../../../utils/migration/Migration';
import { currentDriver, getQualifiedTableDef } from '../../../../dataSources';
@ -597,6 +599,179 @@ const addRelViewMigrate = (tableSchema, toggleEditor) => (
}
};
export const addDbToDbRelationship = (
state,
tableSchema,
toggleEditor,
isNew,
onSuccess
) => (dispatch, getState) => {
const {
relType,
relName,
relSource,
relTable,
relColumns,
relDriver,
} = state;
const currentTableName = tableSchema.table_name;
const currentTableSchema = tableSchema.table_schema;
const isObjRel = relType === 'object';
const { currentDataSource } = getState().tables;
const columnMapping = relColumns.reduce((acc, { column, refColumn }) => {
if (column === '') return acc;
return { ...acc, [column]: refColumn };
}, {});
const tableInfo = getQualifiedTableDef(
{
name: currentTableName,
schema: currentTableSchema,
},
currentDriver
);
const relChangesUp = [
getSaveRemoteDbRelationshipQuery(
isObjRel,
currentTableName,
relName,
relTable,
columnMapping,
currentDataSource,
relSource,
isNew,
relDriver,
currentTableSchema
),
];
const relChangesDown = [
getDropRemoteRelQuery(relName, tableInfo, currentDataSource),
];
// Apply migrations
const migrationName = `create_relationship_${relName}_${currentTableSchema}_table_${currentTableName}`;
const requestMsg = 'Adding Relationship...';
const successMsg = 'Relationship created';
const errorMsg = 'Creating relationship failed';
const customOnSuccess = () => {
onSuccess();
toggleEditor();
};
const customOnError = () => {};
// perform validations and make call
if (!relName.trim()) {
dispatch(
showErrorNotification(
'Error adding relationship!',
'Relationship name cannot be empty'
)
);
} else if (!gqlPattern.test(relName)) {
dispatch(
showErrorNotification(
gqlRelErrorNotif[0],
gqlRelErrorNotif[1],
gqlRelErrorNotif[2]
)
);
} else {
makeMigrationCall(
dispatch,
getState,
relChangesUp,
relChangesDown,
migrationName,
customOnSuccess,
customOnError,
requestMsg,
successMsg,
errorMsg
);
}
};
export const dropDbToDbRelationship = (state, tableSchema, toggleEditor) => (
dispatch,
getState
) => {
if (
!getConfirmation('This will permanently delete the remote db relationship')
) {
return;
}
const {
relType,
relName,
relSource,
relTable,
relColumns,
relDriver,
} = state;
const currentTableName = tableSchema.table_name;
const currentTableSchema = tableSchema.table_schema;
const isObjRel = relType === 'object' ? true : false;
const currentDataSource = getState().tables.currentDataSource;
const columnMapping = relColumns.reduce((acc, { column, refColumn }) => {
if (column === '') return acc;
return { ...acc, [column]: refColumn };
}, {});
const relChangesUp = [
getDropRemoteDbRelationshipQuery(
relName,
currentTableName,
currentDataSource,
currentTableSchema
),
];
const relChangesDown = [
getSaveRemoteDbRelationshipQuery(
isObjRel,
currentTableName,
relName,
relTable,
columnMapping,
currentDataSource,
relSource,
false,
relDriver,
currentTableSchema
),
];
const migrationName = `table_${currentTableName}_drop_remote_relationship_${relName}`;
const requestMsg = 'Deleting remote relationship...';
const successMsg = 'Successfully deleted remote relationship';
const errorMsg = 'Deleting remote relationship failed';
const customOnSuccess = () => {
toggleEditor();
};
const customOnError = () => {};
makeMigrationCall(
dispatch,
getState,
relChangesUp,
relChangesDown,
migrationName,
customOnSuccess,
customOnError,
requestMsg,
successMsg,
errorMsg
);
};
const sanitizeRelName = arg => arg.trim();
const fallBackRelName = (relMeta, existingFields, iterNumber = 0) => {

View File

@ -29,6 +29,7 @@ import { findAllFromRel, isFeatureSupported } from '../../../../dataSources';
import { getRemoteSchemasSelector } from '../../../../metadata/selector';
import { RightContainer } from '../../../Common/Layout/RightContainer';
import FeatureDisabled from '../FeatureDisabled';
import { RemoteDbRelationships } from './RemoteDbRelationships/RemoteDbRelationships';
const addRelationshipCellView = (
dispatch,
@ -461,7 +462,12 @@ const Relationships = ({
return addRelSection;
};
const existingRemoteRelationships = tableSchema.remote_relationships;
const existingRemoteRelationships =
tableSchema?.remote_relationships?.filter(
field =>
'remote_schema' in field.definition ||
'to_remote_schema' in field.definition
) ?? [];
return (
<RightContainer>
@ -487,6 +493,19 @@ const Relationships = ({
{addedRelationshipsView}
{getAddRelSection()}
</div>
{isFeatureSupported(
'tables.relationships.remoteDbRelationships.hostSource'
) ? (
<div
className={`${styles.padd_left_remove} col-xs-10 col-md-10 ${styles.add_mar_bottom}`}
>
<RemoteDbRelationships
tableSchema={tableSchema}
reduxDispatch={dispatch}
currentSource={currentSource}
/>
</div>
) : null}
{isFeatureSupported('tables.relationships.remoteRelationships') ? (
<div className={`${styles.padd_left_remove} col-xs-10 col-md-10`}>
<RemoteRelationships

View File

@ -0,0 +1,106 @@
import React, { useReducer } from 'react';
import { useQueryClient } from 'react-query';
import { RemoteDBRelationship } from '@/metadata/types';
import { NormalizedTable } from '@/dataSources/types';
import { Dispatch } from '@/types';
import { ordinalColSort } from '../../utils';
import { addDbToDbRelationship, dropDbToDbRelationship } from '../Actions';
import {
relResetState,
dbToDbRelDefaultState,
dbToDbRelReducer,
} from './state';
import ExpandableEditor from '../../../../Common/Layout/ExpandableEditor/Editor';
import ManualRelationshipSelector from './ManualRelationshipSelector';
import { RemoteRelCollapsedLabel } from './RemoteRelCollapsedLabel';
import { parseDbToDbRemoteRel } from './utils';
const AddManualRelationship = ({
tableSchema,
reduxDispatch,
currentSource,
relationship,
}: {
tableSchema: NormalizedTable;
reduxDispatch: Dispatch;
currentSource: string;
relationship: RemoteDBRelationship | null;
}) => {
const [state, dispatch] = useReducer(
dbToDbRelReducer,
relationship ? parseDbToDbRemoteRel(relationship) : dbToDbRelDefaultState
);
const columns = tableSchema.columns.sort(ordinalColSort);
const isNew = relationship === null;
const queryClient = useQueryClient();
// columns in the right order with their indices
const orderedColumns = columns.map((c, i) => ({
name: c.column_name,
index: i,
}));
const resetManualRel = () => {
if (!relationship) {
dispatch(relResetState());
}
};
const removeFunc = relationship
? (toggleEditor: unknown) => {
reduxDispatch(dropDbToDbRelationship(state, tableSchema, toggleEditor));
}
: null;
const saveFk = (toggleEditor: unknown) => {
reduxDispatch(
addDbToDbRelationship(state, tableSchema, toggleEditor, isNew, () => {
queryClient.refetchQueries(['metadata'], { active: true });
})
);
// queryClient.refetchQueries(['metadata'], { active: true });
};
const expandedContent = () => (
<ManualRelationshipSelector
orderedColumns={orderedColumns}
state={state}
dispatch={dispatch}
isNew={isNew}
currentSource={currentSource}
/>
);
const collapsedLabel = () => (
<RemoteRelCollapsedLabel
currentSource={currentSource}
currentSchema={tableSchema?.table_schema}
currentTable={tableSchema?.table_name}
relationship={relationship ?? undefined}
/>
);
const expandedLabel = () => {
return <b>Configure relationship</b>;
};
return (
<div key="add_manual_relationship">
<ExpandableEditor
editorExpanded={expandedContent}
expandedLabel={expandedLabel}
expandButtonText={
relationship ? 'Edit' : 'Add a remote database relationship'
}
collapsedLabel={collapsedLabel}
collapseCallback={resetManualRel}
saveFunc={saveFk}
removeFunc={removeFunc}
service="create"
property="manual-remote-db-rel"
/>
</div>
);
};
export default AddManualRelationship;

View File

@ -0,0 +1,285 @@
import React from 'react';
import { getSupportedDrivers } from '@/dataSources';
import styles from '../../../../Common/TableCommon/Table.scss';
import {
relSetDriver,
relSetName,
relSetSource,
relSetTable,
relSetType,
relSetColumns,
} from './state';
import { useTableColumns } from '@/features/SqlQueries/hooks/useTableColumns';
import { getColumnNameArrayFromHookData } from './utils';
import { MetadataSelector, useMetadata } from '@/features/MetadataAPI';
const ColumnSelect = ({ orderedColumns, state, dispatch }) => {
const selectTitle = !state.relTable.name
? 'Please select the reference table'
: undefined;
const query = useTableColumns(state.relSource, state.relTable);
const dispatchSetCols = (key, value, index) => {
const relCols = Object.assign([], state.relColumns);
relCols[index][key] = value;
if (
relCols[state.relColumns.length - 1].column &&
relCols[state.relColumns.length - 1].refColumn
) {
relCols.push({ column: '', refColumn: '' });
}
dispatch(relSetColumns(relCols));
};
const dispatchRemoveCol = index => {
const newColMapping = [
...state.relColumns.slice(0, index),
...state.relColumns.slice(index + 1),
];
dispatch(relSetColumns(newColMapping));
};
return (
<div className={`${styles.add_mar_bottom}`}>
<div className={`row ${styles.add_mar_bottom_mid}`}>
<div className={`col-sm-4 ${styles.add_mar_right}`}>
<b>From:</b>
</div>
<div className={`col-sm-4 ${styles.add_mar_right}`}>
<b>To:</b>
</div>
</div>
{state.relColumns.map((colMap, index) => (
<div
className={`row ${styles.add_mar_bottom_mid} ${styles.display_flex}`}
key={`fk-col-${index}`}
>
<div className={`col-sm-4 ${styles.add_mar_right}`}>
<select
className={`form-control ${styles.select} ${styles.wd100Percent}`}
value={colMap.column}
onChange={e => {
dispatchSetCols('column', e.target.value, index);
}}
data-test={`manual-relationship-lcol-${index}`}
disabled={!state.relType || !state.relName}
title={selectTitle}
>
{colMap.column === '' && (
<option value="" disabled>
{'-- column --'}
</option>
)}
{orderedColumns.map(oc => (
<option key={oc.name} value={oc.name}>
{oc.name}
</option>
))}
</select>
</div>
<div className={'col-sm-4'}>
<select
className={`form-control ${styles.select} ${styles.wd100Percent}`}
value={colMap.refColumn}
onChange={e => {
dispatchSetCols('refColumn', e.target.value, index);
}}
disabled={!state.relTable.name}
title={selectTitle}
data-test={`manual-relationship-rcol-${index}`}
>
{colMap.refColumn === '' && (
<option value="" disabled>
{'-- ref_column --'}
</option>
)}
{query.isSuccess
? getColumnNameArrayFromHookData(query.data).map(rcOpt => (
<option key={rcOpt} value={rcOpt}>
{rcOpt}
</option>
))
: null}
</select>
</div>
<div>
{index + 1 !== state.relColumns.length ? (
<i
className={`${styles.fontAwosomeClose} fa-lg fa fa-times`}
onClick={() => {
dispatchRemoveCol(index);
}}
/>
) : null}
</div>
</div>
))}
</div>
);
};
const ManualRelationshipSelector = ({
orderedColumns,
state,
dispatch,
isNew,
currentSource,
}) => {
const refTables = {};
const { data: source } = useMetadata(
MetadataSelector.getDataSourceMetadata(state.relSource)
);
const { data: driversList } = useMetadata(MetadataSelector.getAllDriversList);
if (source) {
(source.tables ?? []).forEach(x => {
const { schema, dataset, name } = x.table;
refTables[name] = schema ?? dataset;
});
}
const dispatchSetRelType = event => {
dispatch(relSetType(event.target.value));
};
const dispatchSetRelName = event => {
dispatch(relSetName(event.target.value));
};
const dispatchSetRefSource = event => {
dispatch(relSetSource(event.target.value));
dispatch(
relSetTable({
name: '',
schema: '',
})
);
dispatch(relSetColumns([{ column: '', refColumn: '' }]));
};
const dispatchSetRefTable = event => {
dispatch(relSetDriver(source.kind));
dispatch(
relSetTable({
name: event.target.value,
schema: refTables[event.target.value],
})
);
dispatch(relSetColumns([{ column: '', refColumn: '' }]));
};
return (
<div className="form-group">
<div className={`${styles.add_mar_bottom}`}>
<div className={`${styles.add_mar_bottom_mid}`}>
<b>Relationship Type:</b>
</div>
<select
value={state.relType || ''}
className={`${styles.select} form-control ${styles.add_pad_left}`}
data-test={'manual-relationship-type'}
onChange={dispatchSetRelType}
>
{state.relType === '' && (
<option value={''} disabled>
{'-- relationship type --'}
</option>
)}
<option key="object" value="object">
Object Relationship
</option>
<option key="array" value="array">
Array Relationship
</option>
</select>
</div>
<div className={`${styles.add_mar_bottom}`}>
<div className={`${styles.add_mar_bottom_mid}`}>
<b>Relationship Name:</b>
</div>
<input
onChange={dispatchSetRelName}
className={`${styles.select} form-control ${styles.add_pad_left}`}
value={state.relName}
placeholder="Enter relationship name"
disabled={!state.relType || !isNew}
data-test="rel-name"
/>
</div>
<div className={`${styles.add_mar_bottom}`}>
<div className={`${styles.add_mar_bottom_mid}`}>
<b>Reference Source:</b>
</div>
<select
value={state.relSource || ''}
className={`${styles.select} form-control ${styles.add_pad_left}`}
data-test={'manual-relationship-ref-schema'}
onChange={dispatchSetRefSource}
disabled={!state.relType || !state.relName}
>
{
// default unselected option
state.relSource === '' && (
<option value={''} disabled>
{'-- reference source --'}
</option>
)
}
{
// all reference source options
driversList &&
driversList
.filter(
s =>
currentSource !== s.source &&
getSupportedDrivers(
'tables.relationships.remoteDbRelationships.referenceSource'
).includes(s.kind)
)
.map(s => (
<option key={s.source} value={s.source}>
{s.source}
</option>
))
}
</select>
</div>
<div className={`${styles.add_mar_bottom}`}>
<div className={`${styles.add_mar_bottom_mid}`}>
<b>Reference Table:</b>
</div>
<select
value={state.relTable.name || ''}
className={`${styles.select} form-control ${styles.add_pad_left}`}
data-test={'manual-relationship-ref-table'}
onChange={dispatchSetRefTable}
disabled={!state.relType || !state.relName}
>
{state.relTable.name === '' && (
<option value={''} disabled>
{'-- reference table --'}
</option>
)}
{Object.keys(refTables)
.sort()
.map((rt, j) => (
<option key={j} value={rt}>
{rt}
</option>
))}
</select>
</div>
<ColumnSelect
orderedColumns={orderedColumns}
state={state}
dispatch={dispatch}
/>
</div>
);
};
export default ManualRelationshipSelector;

View File

@ -0,0 +1,85 @@
import React from 'react';
import { useRemoteDatabaseRelationships } from '@/features/MetadataAPI';
import { NormalizedTable } from '@/dataSources/types';
import { Dispatch } from '@/types';
import { currentDriver } from '@/dataSources';
import { QualifiedTable } from '@/metadata/types';
import styles from '../../TableModify/ModifyTable.scss';
import ToolTip from '../../../../Common/Tooltip/Tooltip';
import KnowMoreLink from '../../../../Common/KnowMoreLink/KnowMoreLink';
import AddManualRelationship from './AddManualRelationship';
type Props = {
tableSchema: NormalizedTable;
reduxDispatch: Dispatch;
currentSource: string;
};
export const RemoteDbRelationships: React.FC<Props> = ({
tableSchema,
reduxDispatch,
currentSource,
}) => {
let qualifiedTable = {};
if (currentDriver !== 'bigquery') {
qualifiedTable = {
name: tableSchema.table_name,
schema: tableSchema.table_schema,
};
} else {
qualifiedTable = {
name: tableSchema.table_name,
dataset: tableSchema.table_schema,
};
}
const {
isLoading,
isError,
isSuccess,
data,
} = useRemoteDatabaseRelationships(
currentSource,
qualifiedTable as QualifiedTable
); // get data from hook
if (isLoading) {
return <p>Loading</p>;
}
if (isError) {
return <p>Error</p>;
}
return (
<>
{isSuccess ? (
<>
<h4 className={styles.subheading_text}>
Remote Database Relationships
<ToolTip message="Relationships to remote database tables" />
&nbsp;
<KnowMoreLink href="https://hasura.io/docs/latest/graphql/core/schema/table-relationships/index.html" />
</h4>
<div className={styles.activeEdit}>
{data?.map(r => (
<AddManualRelationship
key={r?.name}
tableSchema={tableSchema}
reduxDispatch={reduxDispatch}
currentSource={currentSource}
relationship={r}
/>
))}
<AddManualRelationship
tableSchema={tableSchema}
currentSource={currentSource}
reduxDispatch={reduxDispatch}
relationship={null}
/>
</div>
</>
) : null}
</>
);
};

View File

@ -0,0 +1,36 @@
import { RemoteDBRelationship } from '@/metadata/types';
import React from 'react';
import { parseDbToDbRemoteRel } from './utils';
type Props = {
currentSource: string;
currentSchema: string;
currentTable: string;
relationship?: RemoteDBRelationship;
};
export const RemoteRelCollapsedLabel: React.FC<Props> = ({
currentSource,
currentSchema,
currentTable,
relationship,
}) => {
if (!relationship) {
return null;
}
const parseRelationship = parseDbToDbRemoteRel(relationship);
return (
<div className="flex">
<div>
<b>{`${parseRelationship.relName}`}</b>&nbsp;
</div>
<div>
<i>
{`- ${currentSource}.${currentSchema}.${currentTable}${parseRelationship.relSource}.${parseRelationship.relTable.name}
`}
</i>
</div>
</div>
);
};

View File

@ -0,0 +1,55 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`utils tests getColumnNameArrayFromHookData util function should return 1`] = `
Array [
"id",
"name",
"firstname",
"lastname",
"weight",
]
`;
exports[`utils tests getRelColumnsMapping util function should return 1`] = `
Array [
Object {
"column": "id",
"refColumn": "id",
},
Object {
"column": "name",
"refColumn": "name",
},
Object {
"column": "",
"refColumn": "",
},
]
`;
exports[`utils tests parseDbToDbRemoteRel util function should return 1`] = `
Object {
"relColumns": Array [
Object {
"column": "name",
"refColumn": "name",
},
Object {
"column": "id",
"refColumn": "weight",
},
Object {
"column": "",
"refColumn": "",
},
],
"relDriver": "",
"relName": "aaaaaaa",
"relSource": "remote_db",
"relTable": Object {
"name": "testt",
"schema": "public",
},
"relType": "object",
}
`;

View File

@ -0,0 +1,25 @@
export const column_mapping_input = {
id: 'id',
name: 'name',
};
export const column_name_data = [
['database', 'table_schema', 'table_name', 'column_name', 'data_type'],
['ddf0v5f7prohtg', 'public', 'testt', 'id', 'integer'],
['ddf0v5f7prohtg', 'public', 'testt', 'name', 'text'],
['ddf0v5f7prohtg', 'public', 'testt', 'firstname', 'text'],
['ddf0v5f7prohtg', 'public', 'testt', 'lastname', 'text'],
['ddf0v5f7prohtg', 'public', 'testt', 'weight', 'numeric'],
];
export const parse_rel_data = {
definition: {
to_source: {
relationship_type: 'object',
source: 'remote_db',
table: { schema: 'public', name: 'testt' },
field_mapping: { name: 'name', id: 'weight' },
},
},
name: 'aaaaaaa',
};

View File

@ -0,0 +1,25 @@
import {
getRelColumnsMapping,
getColumnNameArrayFromHookData,
parseDbToDbRemoteRel,
} from '../utils';
import {
column_mapping_input,
column_name_data,
parse_rel_data,
} from './fixtures/input';
describe('utils tests', () => {
test('getRelColumnsMapping util function should return', () => {
const res = getRelColumnsMapping(column_mapping_input);
expect(res).toMatchSnapshot();
});
test('getColumnNameArrayFromHookData util function should return', () => {
const res = getColumnNameArrayFromHookData(column_name_data);
expect(res).toMatchSnapshot();
});
test('parseDbToDbRemoteRel util function should return', () => {
const res = parseDbToDbRemoteRel(parse_rel_data);
expect(res).toMatchSnapshot();
});
});

View File

@ -0,0 +1,121 @@
import { QualifiedTable } from '@/metadata/types';
export const REL_SET_NAME = 'Relationships/REL_SET_NAME';
export const REL_SET_TYPE = 'Relationships/REL_SET_TYPE';
export const REL_SET_SOURCE = 'Relationships/REL_SET_SOURCE';
export const REL_SET_TABLE = 'Relationships/REL_SET_TABLE';
export const REL_SET_COLUMNS = 'Relationships/REL_SET_COLUMNS';
export const REL_SET_STATE = 'Relationships/REL_SET_STATE';
export const REL_RESET_STATE = 'Relationships/REL_RESET_STATE';
export const REL_SET_DRIVER = 'Relationships/REL_SET_DRIVER';
type DbToDbRelState = {
relName: string;
relType: string;
relSource: string;
relTable: QualifiedTable;
relColumns: Record<string, string>[];
relDriver: string;
};
export const relSetName = (relName: string) => ({
type: REL_SET_NAME as typeof REL_SET_NAME,
relName,
});
export const relSetType = (relType: string) => ({
type: REL_SET_TYPE as typeof REL_SET_TYPE,
relType,
});
export const relSetSource = (relSource: string) => ({
type: REL_SET_SOURCE as typeof REL_SET_SOURCE,
relSource,
});
export const relSetTable = (relTable: QualifiedTable) => ({
type: REL_SET_TABLE as typeof REL_SET_TABLE,
relTable,
});
export const relSetColumns = (relColumns: Record<string, string>[]) => ({
type: REL_SET_COLUMNS as typeof REL_SET_COLUMNS,
relColumns,
});
export const relSetState = (relState: DbToDbRelState) => ({
type: REL_SET_STATE as typeof REL_SET_STATE,
relState,
});
export const relResetState = () => ({
type: REL_RESET_STATE as typeof REL_RESET_STATE,
});
export const relSetDriver = (relDriver: string) => ({
type: REL_SET_DRIVER as typeof REL_SET_DRIVER,
relDriver,
});
type DbToDbRelEvents =
| ReturnType<typeof relSetName>
| ReturnType<typeof relSetType>
| ReturnType<typeof relSetSource>
| ReturnType<typeof relSetTable>
| ReturnType<typeof relSetColumns>
| ReturnType<typeof relSetState>
| ReturnType<typeof relSetDriver>
| ReturnType<typeof relResetState>;
export const dbToDbRelDefaultState: DbToDbRelState = {
relName: '',
relType: '',
relSource: '',
relTable: {
name: '',
schema: '',
},
relColumns: [{ column: '', refColumn: '' }],
relDriver: '',
};
export const dbToDbRelReducer = (
state = dbToDbRelDefaultState,
action: DbToDbRelEvents
): DbToDbRelState => {
switch (action.type) {
case REL_SET_NAME:
return {
...state,
relName: action.relName,
};
case REL_SET_TYPE:
return {
...state,
relType: action.relType,
};
case REL_SET_SOURCE:
return {
...state,
relSource: action.relSource,
};
case REL_SET_TABLE:
return {
...state,
relTable: action.relTable,
};
case REL_SET_COLUMNS:
return {
...state,
relColumns: action.relColumns,
};
case REL_SET_STATE:
return {
...action.relState,
};
case REL_RESET_STATE:
return {
...dbToDbRelDefaultState,
};
case REL_SET_DRIVER:
return {
...state,
relDriver: action.relDriver,
};
default:
return state;
}
};

View File

@ -0,0 +1,35 @@
import { isEmpty } from '@/components/Common/utils/jsUtils';
import { RemoteDBRelationship } from '@/metadata/types';
export const getRelColumnsMapping = (fields: Record<string, string>) => {
const colsMapping = Object.entries(fields).map(([k, v]) => ({
column: k,
refColumn: v,
}));
colsMapping.push({ column: '', refColumn: '' });
return colsMapping;
};
export const parseDbToDbRemoteRel = (relationship: RemoteDBRelationship) => {
return {
relName: relationship?.name,
relType: relationship?.definition?.to_source?.relationship_type,
relSource: relationship?.definition?.to_source?.source,
relTable: {
name:
relationship?.definition?.to_source?.table?.name ??
relationship?.definition?.to_source?.table,
schema: relationship?.definition?.to_source?.table?.schema ?? '',
},
relColumns: isEmpty(relationship?.definition?.to_source?.field_mapping)
? [{ column: '', refColumn: '' }]
: getRelColumnsMapping(
relationship?.definition?.to_source?.field_mapping
),
relDriver: '',
};
};
export const getColumnNameArrayFromHookData = (data: string[][]) => {
return data.slice(1).map(d => d[3]);
};

View File

@ -152,6 +152,10 @@ export const supportedFeatures: DeepRequired<SupportedFeaturesType> = {
relationships: {
enabled: true,
track: false,
remoteDbRelationships: {
hostSource: true,
referenceSource: true,
},
remoteRelationships: false,
},
permissions: {

View File

@ -171,6 +171,10 @@ export const supportedFeatures: DeepRequired<SupportedFeaturesType> = {
relationships: {
enabled: true,
track: true,
remoteDbRelationships: {
hostSource: false,
referenceSource: false,
},
remoteRelationships: false,
},
permissions: {

View File

@ -631,6 +631,10 @@ export const supportedFeatures: DeepRequired<SupportedFeaturesType> = {
},
relationships: {
enabled: true,
remoteDbRelationships: {
hostSource: true,
referenceSource: true,
},
remoteRelationships: true,
track: true,
},

View File

@ -338,6 +338,10 @@ export type SupportedFeaturesType = {
};
relationships: {
enabled: boolean;
remoteDbRelationships?: {
hostSource: boolean;
referenceSource: boolean;
};
remoteRelationships?: boolean;
track: boolean;
};

View File

@ -122,4 +122,7 @@ export namespace MetadataSelector {
).filter(field => 'to_remote_schema' in field.definition);
return remote_schema_relationships;
};
export const getAllDriversList = (m: MetadataResponse) =>
m.metadata?.sources.map(s => ({ source: s.name, kind: s.kind }));
}

View File

@ -10,6 +10,10 @@ export const useMetadataTables = () => {
return useMetadata(MetadataSelector.getTables(source));
};
export const useTables = (database: string) => {
return useMetadata(MetadataSelector.getTables(database));
};
export const useRemoteDatabaseRelationships = (
database: string,
table: QualifiedTable

View File

@ -0,0 +1,33 @@
import { ReactQueryDecorator } from '@/storybook/decorators/react-query';
import { ReduxDecorator } from '@/storybook/decorators/redux-decorator';
import ReactJson from 'react-json-view';
import { ComponentMeta, ComponentStory } from '@storybook/react';
import React from 'react';
import { useTables } from '../hooks/useMetadataTables';
function FetchTables({ database }: { database: string }) {
const query = useTables(database);
return (
<div>
{query.isSuccess ? <ReactJson src={query.data} /> : 'no response'}
{query.isError ? <ReactJson src={query.error} /> : null}
</div>
);
}
export const FetchTableColumns: ComponentStory<typeof FetchTables> = args => {
return <FetchTables {...args} />;
};
FetchTableColumns.args = {
database: 'default',
};
export default {
title: 'hooks/Table Queries/Fetch Tables',
decorators: [
ReduxDecorator({ tables: { currentDataSource: 'default' } }),
ReactQueryDecorator(),
],
} as ComponentMeta<typeof FetchTables>;

View File

@ -1,4 +1,5 @@
import type { TableORSchemaArg } from '@/dataSources/types';
import { QualifiedTable } from '@/metadata/types';
import type { DatasourceSqlQueries } from '.';
export const bigquerySqlQueries: DatasourceSqlQueries = {
@ -47,7 +48,9 @@ export const bigquerySqlQueries: DatasourceSqlQueries = {
getFKRelations(): string {
return 'select []';
},
getTableColumnsSql(): string {
return 'not implemented';
getTableColumnsSql({ name, schema }: QualifiedTable): string {
if (!schema || !name) throw Error('empty parameters are not allowed!');
return `SELECT * FROM ${schema}.INFORMATION_SCHEMA.COLUMNS WHERE table_name = '${name}';`;
},
};

View File

@ -194,6 +194,6 @@ FROM sys.objects as obj
getTableColumnsSql: ({ name, schema }: QualifiedTable) => {
if (!name || !schema) throw Error('empty parameters are not allowed!');
return `not implemented`;
return `SELECT * FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = N'${name}' AND TABLE_SCHEMA= N'${schema}'`;
},
};

View File

@ -3,6 +3,7 @@ import { ReactQueryDecorator } from '@/storybook/decorators/react-query';
import { ReduxDecorator } from '@/storybook/decorators/redux-decorator';
import { ComponentMeta, ComponentStory } from '@storybook/react';
import React from 'react';
import ReactJson from 'react-json-view';
import { useTableColumns } from '../useTableColumns';
function FetchTableColumnsComponent({
@ -15,9 +16,9 @@ function FetchTableColumnsComponent({
const query = useTableColumns(database, table);
return (
<div>
{query.isSuccess ? JSON.stringify(query.data, null, 2) : 'no response'}
{query.isSuccess ? <ReactJson src={query.data} /> : 'no response'}
{query.isError ? JSON.stringify(query.error, null, 2) : null}
{query.isError ? <ReactJson src={query.error} /> : null}
</div>
);
}

View File

@ -5,14 +5,17 @@ import {
useMetadata,
useMetadataVersion,
} from '@/features/MetadataAPI';
import { useQuery } from 'react-query';
import { useQuery, UseQueryResult } from 'react-query';
import { useAppSelector } from '@/store';
import { getRunSqlQuery } from '@/components/Common/utils/v1QueryUtils';
import Endpoints from '@/Endpoints';
import { RunSQLResponse } from '@/hooks/types';
import { dataSourceSqlQueries } from '..';
export const useTableColumns = (database: string, table: QualifiedTable) => {
export const useTableColumns = (
database: string,
table: QualifiedTable
): UseQueryResult<Record<string, any>, Error> => {
const { data: source } = useMetadata(
MetadataSelector.getDataSourceMetadata(database)
);
@ -74,6 +77,6 @@ export const useTableColumns = (database: string, table: QualifiedTable) => {
d => parser(d.result ?? [])
);
},
enabled: !!driver,
enabled: !!driver && !!table.name,
});
};

View File

@ -545,6 +545,9 @@ export const getRedeliverDataEventQuery = (
event_id: eventId,
});
// this function returns the payload in old format,
// please note that there is also a new format for local table to remote schema relationship
// https://gist.github.com/0x777/e9c21e846507c6123cfb7a40c64d5772
export const getSaveRemoteRelQuery = (
args: RemoteRelationshipPayload,
isNew: boolean,
@ -665,6 +668,60 @@ export const getCreateArrayRelationshipQuery = (
using: {},
});
export const getSaveRemoteDbRelationshipQuery = (
isObjRel: boolean,
tableName: string,
name: string,
remoteTable: Record<string, string>,
columnMapping: Record<string, string>,
source: string,
rSource: string,
isNew: boolean,
driver: string,
schema: string
) => {
const args = {
source,
name,
table:
currentDriver !== 'bigquery'
? { name: tableName, schema }
: { name: tableName, dataset: schema },
definition: {
to_source: {
source: rSource,
table:
driver !== 'bigquery'
? remoteTable
: { name: remoteTable.name, dataset: remoteTable.schema },
relationship_type: isObjRel ? 'object' : 'array',
field_mapping: columnMapping,
},
},
};
return getMetadataQuery(
isNew ? 'create_remote_relationship' : 'update_remote_relationship',
source,
args
);
};
export const getDropRemoteDbRelationshipQuery = (
name: string,
tableName: string,
source: string,
schema: string
) =>
getMetadataQuery('delete_remote_relationship', source, {
name,
table:
currentDriver !== 'bigquery'
? { name: tableName, schema }
: { name: tableName, dataset: schema },
source,
});
export const getAddRelationshipQuery = (
isObjRel: boolean,
table: QualifiedTable,

View File

@ -589,6 +589,20 @@ export interface RemoteSchemaDef {
/**
* https://hasura.io/docs/latest/graphql/core/api-reference/schema-metadata-api/remote-relationships.html#args-syntax
*/
export interface RemoteDBRelationship {
/** Name of the remote relationship */
name: string;
/** Definition object */
definition: {
to_source: {
relationship_type: string;
source: string;
table: { schema: string; name: string };
field_mapping: Record<string, string>;
};
};
}
export interface RemoteRelationship {
/** Name of the remote relationship */
name: RemoteRelationshipName;
@ -606,6 +620,13 @@ export interface RemoteRelationshipDef {
remote_schema: RemoteSchemaName;
/** The schema tree ending at the field in remote schema which needs to be joined with. */
remote_field: RemoteField;
to_source: {
relationship_type: string;
source: string;
table: QualifiedTable;
field_mapping: Record<string, string>;
};
}
/**