mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-15 09:22:43 +03:00
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:
parent
e053ffe8ec
commit
f611ee232a
@ -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'],
|
||||
},
|
||||
},
|
||||
];
|
@ -1 +1,2 @@
|
||||
export * from './dbToLocalDbRelationship';
|
||||
export * from './dbToRemoteSchemaRelationships';
|
||||
|
@ -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",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
@ -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 }));
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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)),
|
||||
});
|
||||
}
|
||||
|
@ -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 })
|
||||
|
@ -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));
|
||||
}),
|
||||
];
|
63
console/src/features/MetadataAPI/stories/mocks/metadata.ts
Normal file
63
console/src/features/MetadataAPI/stories/mocks/metadata.ts
Normal 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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
@ -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"}]',
|
||||
],
|
||||
],
|
||||
};
|
@ -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;
|
@ -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;
|
@ -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 =
|
||||
|
Loading…
Reference in New Issue
Block a user