-
-
- {title.replace('{{name}}', itemName ?? '')}
+ >
+
+
+
+
+
+ {injectRouteDetails(title, props)}
+
+
{subtitleOverride ?? subtitle}
{subtitle}
{children}
-
- {/* */}
+
);
};
diff --git a/frontend/libs/console/legacy-ce/src/lib/features/Data/LogicalModels/components/route-wrapper-utils.tsx b/frontend/libs/console/legacy-ce/src/lib/features/Data/LogicalModels/components/route-wrapper-utils.tsx
new file mode 100644
index 00000000000..7fe0584faa4
--- /dev/null
+++ b/frontend/libs/console/legacy-ce/src/lib/features/Data/LogicalModels/components/route-wrapper-utils.tsx
@@ -0,0 +1,55 @@
+import startCase from 'lodash/startCase';
+import { BreadcrumbItem } from '../../../../new-components/Breadcrumbs/Breadcrumbs';
+import { RouteWrapperProps } from './RouteWrapper';
+
+export const injectRouteDetails = (
+ path: string,
+ {
+ itemName,
+ itemSourceName,
+ itemTabName,
+ }: Pick
+) => {
+ return path
+ .replace('{{source}}', itemSourceName ?? '')
+ .replace('{{name}}', itemName ?? '')
+ .replace('{{tab}}', itemTabName ?? '');
+};
+
+export const pathsToBreadcrumbs = (
+ paths: string[],
+ props: RouteWrapperProps,
+ _push: (path: string) => void
+): BreadcrumbItem[] =>
+ paths.reduce((prev, path, index, arr) => {
+ // skip source in path
+ if (path === '{{source}}') return prev;
+
+ let title = startCase(injectRouteDetails(path, props));
+
+ //prepend source to the item directly following the source so the source is visible in the breadcrumb but not it's own entry in the crumbs
+ if (arr[index - 1] === '{{source}}') {
+ const source = injectRouteDetails(arr[index - 1], props);
+ title = `${source} / ${title}`;
+ }
+
+ return [
+ ...prev,
+ {
+ title,
+ onClick:
+ index === paths.length - 1
+ ? undefined
+ : () => {
+ const pathIndex = paths.indexOf(path);
+
+ const newPath = injectRouteDetails(
+ `/${paths.slice(0, pathIndex + 1).join('/')}`,
+ props
+ );
+
+ _push(newPath);
+ },
+ },
+ ];
+ }, []);
diff --git a/frontend/libs/console/legacy-ce/src/lib/features/Data/LogicalModels/constants.ts b/frontend/libs/console/legacy-ce/src/lib/features/Data/LogicalModels/constants.ts
index 08de5846309..bca78e04e2c 100644
--- a/frontend/libs/console/legacy-ce/src/lib/features/Data/LogicalModels/constants.ts
+++ b/frontend/libs/console/legacy-ce/src/lib/features/Data/LogicalModels/constants.ts
@@ -14,13 +14,16 @@ export const NATIVE_QUERY_ROUTES = {
title: 'Native Queries',
subtitle: 'Access more queries and operators through SQL on your database',
},
+ // create new native query
'/data/native-queries/create': {
title: 'Create Native Query',
subtitle: 'Access more queries and operators through SQL on your database',
},
- '/data/native-queries/{{source}}/{{name}}': {
+ // edit/view native query:
+ '/data/native-queries/{{source}}/{{name}}/{{tab}}': {
title: '{{name}}',
- subtitle: 'Access more queries and operators through SQL on your database',
+ //setting via the RouteWrapper props for this one so it can be dynamic based on tab
+ subtitle: '',
},
'/data/native-queries/logical-models': {
title: 'Logical Models',
diff --git a/frontend/libs/console/legacy-ce/src/lib/features/Data/LogicalModels/mocks/metadata.ts b/frontend/libs/console/legacy-ce/src/lib/features/Data/LogicalModels/mocks/metadata.ts
index 871c2cac7f2..c5108b2691b 100644
--- a/frontend/libs/console/legacy-ce/src/lib/features/Data/LogicalModels/mocks/metadata.ts
+++ b/frontend/libs/console/legacy-ce/src/lib/features/Data/LogicalModels/mocks/metadata.ts
@@ -1,4 +1,4 @@
-import { Metadata } from '../../../hasura-metadata-types';
+import { LogicalModel, Metadata } from '../../../hasura-metadata-types';
const testQueries = {
postgres: [
@@ -14,6 +14,30 @@ const testQueries = {
returns: 'hello_world2',
root_field_name: 'hello_world_function2',
},
+ {
+ arguments: {},
+ code: 'select "CustomerId", "BillingCity" as "City", "BillingCountry" as "Country" from "public"."Invoice"',
+ returns: 'customer_location_model',
+ root_field_name: 'customer_location',
+ },
+ {
+ arguments: {},
+ code: 'select "CustomerId" as "Id", "FirstName" as "Name" from "public"."Customer"',
+ object_relationships: [
+ {
+ name: 'location',
+ using: {
+ column_mapping: {
+ Id: 'CustomerId',
+ },
+ insertion_order: null,
+ remote_native_query: 'customer_location',
+ },
+ },
+ ],
+ returns: 'customer_model',
+ root_field_name: 'customer_native_query',
+ },
],
mssql: [
{
@@ -31,19 +55,17 @@ const testQueries = {
],
};
-const testModels = {
+const testModels: Record = {
postgres: [
{
fields: [
{
name: 'one',
- nullable: false,
- type: 'text',
+ type: { scalar: 'string', nullable: true },
},
{
name: 'two',
- nullable: false,
- type: 'text',
+ type: { scalar: 'string', nullable: true },
},
],
name: 'hello_world',
@@ -52,30 +74,72 @@ const testModels = {
fields: [
{
name: 'one',
- nullable: false,
- type: 'text',
+ type: { scalar: 'string', nullable: true },
},
{
name: 'two',
- nullable: false,
- type: 'text',
+ type: { scalar: 'string', nullable: true },
},
],
name: 'hello_world2',
},
+ // for testing relationships:
+ {
+ fields: [
+ {
+ name: 'CustomerId',
+ type: {
+ nullable: false,
+ scalar: 'integer',
+ },
+ },
+ {
+ name: 'City',
+ type: {
+ nullable: true,
+ scalar: 'varchar',
+ },
+ },
+ {
+ name: 'Country',
+ type: {
+ nullable: true,
+ scalar: 'varchar',
+ },
+ },
+ ],
+ name: 'customer_location_model',
+ },
+ {
+ fields: [
+ {
+ name: 'Id',
+ type: {
+ nullable: false,
+ scalar: 'integer',
+ },
+ },
+ {
+ name: 'Name',
+ type: {
+ nullable: true,
+ scalar: 'varchar',
+ },
+ },
+ ],
+ name: 'customer_model',
+ },
],
mssql: [
{
fields: [
{
name: 'one',
- nullable: false,
- type: 'text',
+ type: { scalar: 'string', nullable: true },
},
{
name: 'two',
- nullable: false,
- type: 'text',
+ type: { scalar: 'string', nullable: true },
},
],
name: 'hello_mssql',
@@ -84,13 +148,11 @@ const testModels = {
fields: [
{
name: 'one',
- nullable: false,
- type: 'text',
+ type: { scalar: 'string', nullable: true },
},
{
name: 'two',
- nullable: false,
- type: 'text',
+ type: { scalar: 'string', nullable: true },
},
],
name: 'hello_mssql2',
diff --git a/frontend/libs/console/legacy-ce/src/lib/features/Data/hooks/useTrackNativeQuery.ts b/frontend/libs/console/legacy-ce/src/lib/features/Data/hooks/useTrackNativeQuery.ts
index 8359fdcc985..d84f43cae70 100644
--- a/frontend/libs/console/legacy-ce/src/lib/features/Data/hooks/useTrackNativeQuery.ts
+++ b/frontend/libs/console/legacy-ce/src/lib/features/Data/hooks/useTrackNativeQuery.ts
@@ -3,6 +3,7 @@ import { useMetadataMigration } from '../../MetadataAPI';
import { MetadataMigrationOptions } from '../../MetadataAPI/hooks/useMetadataMigration';
import { useMetadata } from '../../hasura-metadata-api';
import { NativeQuery, Source } from '../../hasura-metadata-types';
+import { NativeQueryMigrationBuilder } from '../LogicalModels/MigrationBuilder';
import { getSourceDriver } from './utils';
export type TrackNativeQuery = {
@@ -35,16 +36,39 @@ export const useTrackNativeQuery = (
const trackNativeQuery = async ({
data: args,
+ editDetails,
...options
}: {
data: TrackNativeQuery;
+ editDetails?: { rootFieldName: string };
} & MetadataMigrationOptions) => {
+ const { source, ...nativeQuery } = args;
+ const driver = getSourceDriver(sources, args.source);
+
+ if (!driver) {
+ throw new Error('Source could not be found. Unable to identify driver.');
+ }
+
+ const builder = new NativeQueryMigrationBuilder({
+ dataSourceName: source,
+ driver,
+ nativeQuery,
+ });
+
+ // we need the untrack command to use the old root_field_name
+ // if a user is editing the native query, there's a chance that the root field name changed
+ // so, we have to manually set that when using untrack()
+ const argz = editDetails
+ ? builder.untrack(editDetails.rootFieldName).track().payload()
+ : builder.track().payload();
+
mutate(
{
query: {
resource_version,
- type: `${getSourceDriver(sources, args.source)}_track_native_query`,
- args,
+ type: 'bulk_atomic',
+
+ args: argz,
},
},
options
diff --git a/frontend/libs/console/legacy-ce/src/lib/features/Data/hooks/useTrackNativeQueryRelationships/useTrackNativeQueryRelationships.tsx b/frontend/libs/console/legacy-ce/src/lib/features/Data/hooks/useTrackNativeQueryRelationships/useTrackNativeQueryRelationships.tsx
new file mode 100644
index 00000000000..4eeb86c754a
--- /dev/null
+++ b/frontend/libs/console/legacy-ce/src/lib/features/Data/hooks/useTrackNativeQueryRelationships/useTrackNativeQueryRelationships.tsx
@@ -0,0 +1,134 @@
+import { transformErrorResponse } from '../../../ConnectDBRedesign/utils';
+import { useMetadataMigration } from '../../../MetadataAPI';
+import { MetadataMigrationOptions } from '../../../MetadataAPI/hooks/useMetadataMigration';
+import { MetadataSelectors, useMetadata } from '../../../hasura-metadata-api';
+import {
+ NativeQuery,
+ NativeQueryRelationship,
+ Source,
+} from '../../../hasura-metadata-types';
+import { NativeQueryMigrationBuilder } from '../../LogicalModels/MigrationBuilder';
+
+export type TrackNativeQueryRelationshipsProps = NativeQueryRelationship & {
+ type: 'object' | 'array';
+};
+
+export type UntrackNativeQuery = { source: Source } & Pick<
+ NativeQuery,
+ 'root_field_name'
+>;
+
+export const useTrackNativeQueryRelationships = (
+ dataSourceName: string,
+ nativeQueryName: string,
+ globalMutateOptions?: MetadataMigrationOptions
+) => {
+ /**
+ * Get the required metadata variables - sources & resource_version
+ */
+ const { data: { driver, originNativeQuery, resource_version } = {} } =
+ useMetadata(m => ({
+ driver: MetadataSelectors.findSource(dataSourceName)(m)?.kind,
+ originNativeQuery: MetadataSelectors.findSource(dataSourceName)(
+ m
+ )?.native_queries?.find(nq => nq.root_field_name === nativeQueryName),
+ resource_version: m.resource_version,
+ }));
+
+ const { mutate, ...rest } = useMetadataMigration({
+ ...globalMutateOptions,
+ errorTransform: transformErrorResponse,
+ onSuccess: (data, variable, ctx) => {
+ globalMutateOptions?.onSuccess?.(data, variable, ctx);
+ },
+ });
+
+ const trackNativeQueryRelationship = async ({
+ data: args,
+ editDetails,
+ ...options
+ }: {
+ data: TrackNativeQueryRelationshipsProps;
+ // these are the original name/type before edit.
+ // we need these b/c if the user alters either one, we have to do some special handling
+ editDetails?: { name: string; type: 'object' | 'array' };
+ } & MetadataMigrationOptions) => {
+ if (!driver || !originNativeQuery)
+ throw Error('Driver/Native Query not found');
+
+ const { type, ...relationshipDetails } = args;
+
+ const nativeQueryPayload = new NativeQueryMigrationBuilder({
+ driver,
+ nativeQuery: originNativeQuery,
+ dataSourceName,
+ }).untrack();
+
+ if (editDetails) {
+ // if editing, remove the relationship matching the original name and type prior to edit
+ // if a user changed the type or name, we need the originals to make sure we are dropping the right object from the correct array of relationships
+ nativeQueryPayload.removeRelationship(editDetails.type, editDetails.name);
+ }
+
+ nativeQueryPayload
+ // add a relationship to the native query with the details passed in
+ .addRelationship(type, relationshipDetails)
+ // add a track call
+ .track();
+
+ mutate(
+ {
+ query: {
+ resource_version,
+ type: `bulk_atomic`,
+ args: nativeQueryPayload.payload(),
+ },
+ },
+ options
+ );
+ };
+
+ const untrackNativeQueryRelationship = async ({
+ data: { name: relationshipName, type },
+ ...options
+ }: {
+ data: Pick;
+ } & MetadataMigrationOptions) => {
+ if (!driver || !originNativeQuery)
+ throw Error('Driver/Native Query not found');
+
+ const relationship = originNativeQuery[`${type}_relationships`]?.find(
+ n => n.name === relationshipName
+ );
+
+ if (!relationship) {
+ throw new Error('Unable to find relationship');
+ }
+
+ const nativeQueryPayload = new NativeQueryMigrationBuilder({
+ driver,
+ nativeQuery: originNativeQuery,
+ dataSourceName,
+ })
+ .untrack()
+ .removeRelationship(type, relationshipName)
+ .track();
+
+ mutate(
+ {
+ query: {
+ resource_version,
+ type: `bulk_atomic`,
+ args: nativeQueryPayload.payload(),
+ },
+ },
+ options
+ );
+ };
+
+ return {
+ trackNativeQueryRelationship,
+ untrackNativeQueryRelationship,
+ ...rest,
+ };
+};
diff --git a/frontend/libs/console/legacy-ce/src/lib/features/hasura-metadata-api/selectors.ts b/frontend/libs/console/legacy-ce/src/lib/features/hasura-metadata-api/selectors.ts
index cc8dd0637d9..a57a11586b9 100644
--- a/frontend/libs/console/legacy-ce/src/lib/features/hasura-metadata-api/selectors.ts
+++ b/frontend/libs/console/legacy-ce/src/lib/features/hasura-metadata-api/selectors.ts
@@ -27,6 +27,12 @@ export const getSources = () => (m: Metadata) => m?.metadata.sources;
export const findSource = (dataSourceName: string) => (m: Metadata) =>
utils.findMetadataSource(dataSourceName, m);
+export const findNativeQuery =
+ (dataSourceName: string, nativeQueryName: string) => (m: Metadata) =>
+ utils
+ .findMetadataSource(dataSourceName, m)
+ ?.native_queries?.find(nq => nq.root_field_name === nativeQueryName);
+
export const getTables = (dataSourceName: string) => (m: Metadata) =>
utils.findMetadataSource(dataSourceName, m)?.tables;
diff --git a/frontend/libs/console/legacy-ce/src/lib/features/hasura-metadata-types/source/nativeQuery.ts b/frontend/libs/console/legacy-ce/src/lib/features/hasura-metadata-types/source/nativeQuery.ts
index 365b76c67e9..b5b9e861a61 100644
--- a/frontend/libs/console/legacy-ce/src/lib/features/hasura-metadata-types/source/nativeQuery.ts
+++ b/frontend/libs/console/legacy-ce/src/lib/features/hasura-metadata-types/source/nativeQuery.ts
@@ -4,6 +4,15 @@ export type NativeQueryArgument = {
nullable?: boolean;
};
+export type NativeQueryRelationship = {
+ name: string;
+ using: {
+ column_mapping: Record;
+ insertion_order: 'before_parent' | 'after_parent' | null;
+ remote_native_query: string;
+ };
+};
+
export type NativeQuery = {
root_field_name: string;
code: string;
@@ -11,4 +20,6 @@ export type NativeQuery = {
arguments?: Record;
type?: 'query' | 'mutation'; // only query supported for now
comment?: string;
+ object_relationships?: NativeQueryRelationship[];
+ array_relationships?: NativeQueryRelationship[];
};
diff --git a/frontend/libs/console/legacy-ce/src/lib/features/hasura-metadata-types/source/source.ts b/frontend/libs/console/legacy-ce/src/lib/features/hasura-metadata-types/source/source.ts
index 9487f7e1708..2080840f437 100644
--- a/frontend/libs/console/legacy-ce/src/lib/features/hasura-metadata-types/source/source.ts
+++ b/frontend/libs/console/legacy-ce/src/lib/features/hasura-metadata-types/source/source.ts
@@ -112,3 +112,5 @@ export type BulkKeepGoingResponse = [
path: string;
}
];
+
+export { NativeQueryRelationship } from './nativeQuery';
diff --git a/frontend/libs/console/legacy-ce/src/lib/metadata/queryUtils.ts b/frontend/libs/console/legacy-ce/src/lib/metadata/queryUtils.ts
index d1867271391..7e19ad9d67d 100644
--- a/frontend/libs/console/legacy-ce/src/lib/metadata/queryUtils.ts
+++ b/frontend/libs/console/legacy-ce/src/lib/metadata/queryUtils.ts
@@ -113,6 +113,7 @@ export const metadataQueryTypes = [
'untrack_tables',
'track_stored_procedure',
'untrack_stored_procedure',
+ 'bulk_atomic',
] as const;
export type MetadataQueryType = (typeof metadataQueryTypes)[number];
diff --git a/frontend/libs/console/legacy-ce/src/lib/utils/StoryUtils.tsx b/frontend/libs/console/legacy-ce/src/lib/utils/StoryUtils.tsx
index c50e31215c3..68d6e4fe283 100644
--- a/frontend/libs/console/legacy-ce/src/lib/utils/StoryUtils.tsx
+++ b/frontend/libs/console/legacy-ce/src/lib/utils/StoryUtils.tsx
@@ -70,3 +70,9 @@ export const cancelAlert = async (
timeout: removalTimout,
});
};
+
+export const useIsStorybook = () => {
+ return {
+ isStorybook: !!process.env.STORYBOOK,
+ };
+};