CON-151-create-db-to-local-relationship-hook-for-object-and-array-relationships

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/4472
GitOrigin-RevId: e10abcfa861067ba101348d0cd4e670b8dcbadf8
This commit is contained in:
Matt Hardman 2022-05-31 18:43:31 +07:00 committed by hasura-bot
parent e053ffe8ec
commit f611ee232a
13 changed files with 458 additions and 7 deletions

View File

@ -0,0 +1,39 @@
import type { ArrayRelationship, ObjectRelationship } from '@/metadata/types';
export const dbToLocalDbRelationship = {
objectRelationships: [
{
name: 'product_user',
using: {
foreign_key_constraint_on: 'fk_user_id',
},
},
] as ObjectRelationship[],
arrayRelationships: [
{
name: 'user_product',
using: {
foreign_key_constraint_on: {
column: 'fk_user_id',
table: {
schema: 'public',
name: 'product',
},
},
},
},
] as ArrayRelationship[],
};
export const tableRelationships = [
{
from: {
table: 'product',
column: ['fk_user_id'],
},
to: {
table: '"user"',
column: ['id'],
},
},
];

View File

@ -1 +1,2 @@
export * from './dbToLocalDbRelationship';
export * from './dbToRemoteSchemaRelationships';

View File

@ -1,5 +1,9 @@
import { MetadataTransformer } from '../../hooks/metadataTransformers';
import { dbToRemoteSchemaRelationships } from './__fixtures__';
import {
dbToLocalDbRelationship,
dbToRemoteSchemaRelationships,
tableRelationships,
} from './__fixtures__';
test('transformDbToRemoteSchema returns new and legacy formats consistently', () => {
const result = MetadataTransformer.transformDbToRemoteSchema(
@ -57,3 +61,36 @@ test('transformDbToRemoteSchema returns new and legacy formats consistently', ()
]
`);
});
test('transformDbToLocal returns object relationships correctly', () => {
const result = MetadataTransformer.transformTableRelationships({
target: { table: 'product', schema: 'public', database: 'default' },
relationships: {
objectRelationships: dbToLocalDbRelationship.objectRelationships,
arrayRelationships: [],
},
tableRelationships,
});
expect(result).toMatchInlineSnapshot(`
Array [
Object {
"comment": undefined,
"from": Object {
"column": Array [
"fk_user_id",
],
"table": "product",
},
"name": "product_user",
"to": Object {
"column": Array [
"id",
],
"table": "\\"user\\"",
},
"type": "object",
},
]
`);
});

View File

@ -227,6 +227,22 @@ export namespace MetadataSelector {
return array_relationships;
};
export const createGetLocalDBRelationships = (
currentDataSource: string,
table: QualifiedTable
) => (m: MetadataResponse) => {
const metadataTable = getTable(currentDataSource, table)(m);
const objectRelationships: ObjectRelationship[] =
metadataTable?.object_relationships ?? [];
const arrayRelationships: ArrayRelationship[] =
metadataTable?.array_relationships ?? [];
return {
objectRelationships,
arrayRelationships,
};
};
export const getAllDriversList = (m: MetadataResponse) =>
m.metadata?.sources.map(s => ({ source: s.name, kind: s.kind }));
}

View File

@ -1,6 +1,15 @@
import { DataTarget } from '@/features/Datasources';
import { RemoteRelationship, ToRemoteSchema } from '@/metadata/types';
import { DbToDbRelationship, DbToRemoteSchemaRelationship } from '../types';
import {
ArrayRelationship,
ObjectRelationship,
RemoteRelationship,
ToRemoteSchema,
} from '@/metadata/types';
import {
TableRelationship,
DbToDbRelationship,
DbToRemoteSchemaRelationship,
} from '../types';
interface TransformDbToRemoteSchemaArgs {
target: DataTarget;
@ -8,6 +17,57 @@ interface TransformDbToRemoteSchemaArgs {
}
export namespace MetadataTransformer {
// take object and array relationships input
export const transformTableRelationships = ({
target,
relationships,
tableRelationships,
}: {
target: DataTarget;
relationships: {
objectRelationships: ObjectRelationship[];
arrayRelationships: ArrayRelationship[];
};
tableRelationships?: {
from: {
table: string;
column: string[];
};
to: {
table: string;
column: string[];
};
}[];
}) => {
const objs = relationships.objectRelationships.map(({ name, comment }) => {
const tableRelationship = tableRelationships?.find(
({ from }) => from.table === target.table
);
return {
name,
comment,
type: 'object',
...tableRelationship,
};
});
const arrs = relationships.arrayRelationships.map(({ name, comment }) => {
const tableRelationship = tableRelationships?.find(
({ to }) => to.table.replace(/['"]+/g, '') === target.table
);
return {
name,
comment,
type: 'array',
...tableRelationship,
};
});
return [...objs, ...arrs] as TableRelationship[];
};
export const transformDbToRemoteSchema = ({
target,
remote_relationships,

View File

@ -1,15 +1,33 @@
import Endpoints from '@/Endpoints';
import { Api } from '@/hooks/apiUtils';
import { useAppSelector } from '@/store';
import { useQuery, UseQueryOptions } from 'react-query';
import { useQuery, UseQueryOptions, UseQueryResult } from 'react-query';
import type { MetadataResponse } from '../types';
// overloads
export function useMetadata(): UseQueryResult<MetadataResponse, Error>;
export function useMetadata<T extends (d: MetadataResponse) => any>(
select: T = (((d: MetadataResponse) => d) as unknown) as T,
select: T
): UseQueryResult<ReturnType<T>, Error>;
export function useMetadata<
T extends (d: MetadataResponse) => any,
D extends (d: ReturnType<T>) => any
>(
select: T,
transformFn: D,
queryOptions?: Omit<
UseQueryOptions<MetadataResponse, Error, ReturnType<T>, 'metadata'>,
'queryKey' | 'queryFn'
>
): UseQueryResult<ReturnType<D>, Error>;
export function useMetadata(
select = (d: MetadataResponse) => d,
transformFn = (d: unknown) => d,
queryOptions?: Omit<
UseQueryOptions<MetadataResponse, Error, unknown, 'metadata'>,
'queryKey' | 'queryFn'
>
) {
const body = {
type: 'export_metadata',
@ -30,6 +48,6 @@ export function useMetadata<T extends (d: MetadataResponse) => any>(
queryKey: 'metadata',
queryFn,
...queryOptions,
select,
select: d => transformFn(select(d)),
});
}

View File

@ -1,6 +1,8 @@
import { DataTarget } from '@/features/Datasources';
import { useTableRelationships, DataTarget } from '@/features/Datasources';
import { QualifiedTable } from '@/metadata/types';
import { MetadataSelector } from './metadataSelectors';
import { MetadataTransformer } from './metadataTransformers';
import { useMetadata } from './useMetadata';
export const useMetadataTables = (dataSource: string) => {
@ -11,6 +13,42 @@ export const useTables = (database: string) => {
return useMetadata(MetadataSelector.getTables(database));
};
export const useExistingRelationships = (
database: string,
table: QualifiedTable
) => {
// metadata doesn't contain sufficient info
// therefore have to get full table relationship info
const {
data: tableRelationships,
isLoading: loadingTableRelationships,
error: tableRelationshipsError,
} = useTableRelationships({
target: { database, schema: table.schema, table: table.name },
});
const { error, isLoading: loadingMetadata, ...rest } = useMetadata(
MetadataSelector.createGetLocalDBRelationships(database, table),
relationships =>
MetadataTransformer.transformTableRelationships({
target: {
database,
table: table.name,
schema: table.schema,
},
relationships,
tableRelationships,
}),
{
enabled: !!tableRelationships?.length,
}
);
const isLoading = loadingMetadata || loadingTableRelationships;
return { ...rest, isLoading, error: error || tableRelationshipsError };
};
export const useRemoteDatabaseRelationships = (target: DataTarget) => {
return useMetadata(
MetadataSelector.getRemoteDatabaseRelationships({ target })

View File

@ -0,0 +1,15 @@
import { rest } from 'msw';
import { metadata } from './metadata';
import { queryData } from './querydata';
const baseUrl = 'http://localhost:8080';
export const handlers = (url = baseUrl) => [
rest.post(`${url}/v2/query`, (req, res, ctx) => {
return res(ctx.json(queryData));
}),
rest.post(`${url}/v1/metadata`, (req, res, ctx) => {
return res(ctx.json(metadata));
}),
];

View File

@ -0,0 +1,63 @@
export const metadata = {
resource_version: 1,
metadata: {
version: 3,
sources: [
{
name: 'default',
kind: 'postgres',
tables: [
{
table: {
schema: 'public',
name: 'user',
},
array_relationships: [
{
name: 'products',
using: {
foreign_key_constraint_on: {
column: 'fk_user_id',
table: {
schema: 'public',
name: 'product',
},
},
},
},
],
},
{
table: {
schema: 'public',
name: 'product',
},
object_relationships: [
{
name: 'user',
using: {
foreign_key_constraint_on: 'fk_user_id',
},
},
],
},
],
configuration: {
connection_info: {
use_prepared_statements: true,
database_url: {
from_env: 'HASURA_GRAPHQL_DATABASE_URL',
},
isolation_level: 'read-committed',
pool_settings: {
connection_lifetime: 600,
retries: 1,
idle_timeout: 180,
max_connections: 50,
},
},
},
},
],
},
};

View File

@ -0,0 +1,9 @@
export const queryData = {
result_type: 'TuplesOk',
result: [
['coalesce'],
[
'[{"table_schema":"public","table_name":"product","constraint_name":"sub_stuff_fk_stuff_id_fkey","ref_table_table_schema":"public","ref_table":"user","column_mapping":{ "fk_user_id" : "id" },"on_update":"r","on_delete":"r"}]',
],
],
};

View File

@ -0,0 +1,57 @@
import { ReactQueryDecorator } from '@/storybook/decorators/react-query';
import { ReduxDecorator } from '@/storybook/decorators/redux-decorator';
import ReactJson from 'react-json-view';
import { Meta, Story } from '@storybook/react';
import React from 'react';
import { useExistingRelationships } from '../hooks/useMetadataTables';
import { handlers } from './mocks/handlers.mock';
function UseLocalRelationships() {
const { data: arrayRelationship } = useExistingRelationships('default', {
name: 'user',
schema: 'public',
});
const { data: objectRelationship } = useExistingRelationships('default', {
name: 'product',
schema: 'public',
});
return (
<div>
<p>Array relationships</p>
{arrayRelationship ? (
<ReactJson src={arrayRelationship} />
) : (
'no response'
)}
<p>Object relationships</p>
{objectRelationship ? (
<ReactJson src={objectRelationship} />
) : (
'no response'
)}
</div>
);
}
export const Primary: Story = () => {
return <UseLocalRelationships />;
};
Primary.args = {
database: 'default',
};
export default {
title: 'hooks/useExistingRelationships',
decorators: [
ReduxDecorator({ tables: { currentDataSource: 'default' } }),
ReactQueryDecorator(),
],
parameters: {
msw: handlers(),
},
} as Meta;

View File

@ -0,0 +1,82 @@
import { ReactQueryDecorator } from '@/storybook/decorators/react-query';
import { ReduxDecorator } from '@/storybook/decorators/redux-decorator';
import ReactJson from 'react-json-view';
import { Meta, Story } from '@storybook/react';
import React from 'react';
import { useMetadata } from '../hooks/useMetadata';
import { MetadataSelector } from '../hooks/metadataSelectors';
import { handlers } from './mocks/handlers.mock';
// test transformer to ensure type checking works
const testTransformer = (
args: {
source: string;
kind: 'postgres' | 'mysql' | 'mssql' | 'bigquery' | 'citus';
}[]
) => {
return args.map(({ source, kind }) => ({
transformedSource: `transformed ${source}`,
transformedKind: `transformed ${kind}`,
}));
};
function UseMetadata() {
// use metadata can be used:
// without any arguments -> returns all metadata
const queryNoInput = useMetadata();
// with a selector -> returns a "chunk" of metadata (this should still be formatted as per the metadata spec)
const queryMetadataSelector = useMetadata(MetadataSelector.getAllDriversList);
// with a selector and a transformer -> returns a "chunk" from the selector
// and then transforms it into a relevant shape that the console understands
const queryMetadataSelectorWithTransformer = useMetadata(
MetadataSelector.getAllDriversList,
testTransformer
);
const error =
queryNoInput.error ||
queryMetadataSelector.error ||
queryMetadataSelectorWithTransformer.error;
return (
<div>
{queryNoInput.isSuccess ? (
<ReactJson collapsed src={queryNoInput.data} />
) : (
'no response'
)}
{queryMetadataSelector.isSuccess ? (
<ReactJson src={queryMetadataSelector.data} />
) : (
'no response'
)}
{queryMetadataSelectorWithTransformer.isSuccess ? (
<ReactJson src={queryMetadataSelectorWithTransformer.data} />
) : (
'no response'
)}
{error ? <ReactJson src={error} /> : null}
</div>
);
}
export const Primary: Story = () => {
return <UseMetadata />;
};
Primary.args = {
database: 'default',
};
export default {
title: 'hooks/useMetadata',
decorators: [
ReduxDecorator({ tables: { currentDataSource: 'default' } }),
ReactQueryDecorator(),
],
parameters: {
msw: handlers(),
},
} as Meta;

View File

@ -63,6 +63,22 @@ type SupportedDataSourcesPrefix =
| 'citus_'
| 'pg_';
export interface TableRelationship {
name: string;
comment: string;
type: 'object' | 'array';
tableRelationships?: {
from: {
table: string;
column: string[];
};
to: {
table: string;
column: string[];
};
}[];
}
export type AllMetadataQueries = `${SupportedDataSourcesPrefix}${MetadataQueryType}`;
export type allowedMetadataTypes =