mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-14 08:02:15 +03:00
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:
parent
780d942fad
commit
4453745c57
@ -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
|
||||
|
116
console/package-lock.json
generated
116
console/package-lock.json
generated
@ -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"
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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) => {
|
||||
|
@ -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
|
||||
|
@ -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;
|
@ -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;
|
@ -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" />
|
||||
|
||||
<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}
|
||||
</>
|
||||
);
|
||||
};
|
@ -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>
|
||||
</div>
|
||||
<div>
|
||||
<i>
|
||||
{`- ${currentSource}.${currentSchema}.${currentTable} → ${parseRelationship.relSource}.${parseRelationship.relTable.name}
|
||||
`}
|
||||
</i>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -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",
|
||||
}
|
||||
`;
|
@ -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',
|
||||
};
|
@ -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();
|
||||
});
|
||||
});
|
@ -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;
|
||||
}
|
||||
};
|
@ -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]);
|
||||
};
|
@ -152,6 +152,10 @@ export const supportedFeatures: DeepRequired<SupportedFeaturesType> = {
|
||||
relationships: {
|
||||
enabled: true,
|
||||
track: false,
|
||||
remoteDbRelationships: {
|
||||
hostSource: true,
|
||||
referenceSource: true,
|
||||
},
|
||||
remoteRelationships: false,
|
||||
},
|
||||
permissions: {
|
||||
|
@ -171,6 +171,10 @@ export const supportedFeatures: DeepRequired<SupportedFeaturesType> = {
|
||||
relationships: {
|
||||
enabled: true,
|
||||
track: true,
|
||||
remoteDbRelationships: {
|
||||
hostSource: false,
|
||||
referenceSource: false,
|
||||
},
|
||||
remoteRelationships: false,
|
||||
},
|
||||
permissions: {
|
||||
|
@ -631,6 +631,10 @@ export const supportedFeatures: DeepRequired<SupportedFeaturesType> = {
|
||||
},
|
||||
relationships: {
|
||||
enabled: true,
|
||||
remoteDbRelationships: {
|
||||
hostSource: true,
|
||||
referenceSource: true,
|
||||
},
|
||||
remoteRelationships: true,
|
||||
track: true,
|
||||
},
|
||||
|
@ -338,6 +338,10 @@ export type SupportedFeaturesType = {
|
||||
};
|
||||
relationships: {
|
||||
enabled: boolean;
|
||||
remoteDbRelationships?: {
|
||||
hostSource: boolean;
|
||||
referenceSource: boolean;
|
||||
};
|
||||
remoteRelationships?: boolean;
|
||||
track: boolean;
|
||||
};
|
||||
|
@ -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 }));
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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>;
|
@ -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}';`;
|
||||
},
|
||||
};
|
||||
|
@ -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}'`;
|
||||
},
|
||||
};
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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,
|
||||
});
|
||||
};
|
||||
|
@ -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,
|
||||
|
@ -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>;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
Loading…
Reference in New Issue
Block a user