fix: mssql support for create rest endpoint feature

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/9876
GitOrigin-RevId: f3a027a872e70c8c94b6b6b081644c30bcfc72cb
This commit is contained in:
Daniele Cammareri 2023-07-19 10:45:56 +02:00 committed by hasura-bot
parent 4f6ef10e17
commit a9468f620d
4 changed files with 205 additions and 93 deletions

View File

@ -38,6 +38,14 @@ export const RestEndpointModal = (props: RestEndpointModalProps) => {
[] []
); );
const filteredEndpoints = React.useMemo(
() =>
ENDPOINTS.filter(
method => !!tableEndpointDefinitions[method.value as EndpointType]
),
[tableEndpointDefinitions]
);
return ( return (
<Dialog <Dialog
hasBackdrop hasBackdrop
@ -86,82 +94,103 @@ export const RestEndpointModal = (props: RestEndpointModalProps) => {
} }
> >
<div className="p-4 flex flex-col gap-4"> <div className="p-4 flex flex-col gap-4">
<CardedTable {filteredEndpoints.length > 0 && (
columns={[ <CardedTable
<Checkbox columns={[
checked={selectedMethods.length === ENDPOINTS.length}
onCheckedChange={checked => {
if (checked) {
setSelectedMethods(ENDPOINTS.map(endpoint => endpoint.value));
} else {
setSelectedMethods([]);
}
}}
/>,
'OPERATION',
'METHOD',
'PATH',
]}
data={ENDPOINTS.map(method => {
const endpointDefinition =
tableEndpointDefinitions[method.value as EndpointType];
return [
<Checkbox <Checkbox
checked={selectedMethods.includes(method.value as EndpointType)} checked={selectedMethods.length === ENDPOINTS.length}
disabled={endpointDefinition?.exists}
onCheckedChange={checked => { onCheckedChange={checked => {
if (checked) { if (checked) {
setSelectedMethods([
...selectedMethods,
method.value as EndpointType,
]);
} else {
setSelectedMethods( setSelectedMethods(
selectedMethods.filter( ENDPOINTS.map(endpoint => endpoint.value)
selectedMethod => selectedMethod !== method.value
)
); );
} else {
setSelectedMethods([]);
} }
}} }}
/>, />,
'OPERATION',
'METHOD',
'PATH',
]}
data={filteredEndpoints.map(method => {
const endpointDefinition =
tableEndpointDefinitions[method.value as EndpointType];
return [
<Checkbox
checked={selectedMethods.includes(
method.value as EndpointType
)}
disabled={endpointDefinition?.exists}
onCheckedChange={checked => {
if (checked) {
setSelectedMethods([
...selectedMethods,
method.value as EndpointType,
]);
} else {
setSelectedMethods(
selectedMethods.filter(
selectedMethod => selectedMethod !== method.value
)
);
}
}}
/>,
<div>
{endpointDefinition?.exists ? (
<Link
to={{
pathname: `/api/rest/details/${endpointDefinition.restEndpoint?.name}`,
state: {
...endpointDefinition.restEndpoint,
currentQuery: endpointDefinition.query.query,
},
}}
>
{method.label}{' '}
<FaExternalLinkAlt className="relative ml-1 -top-0.5" />
</Link>
) : (
method.label
)}
</div>,
<Badge color={method.color}>
{endpointDefinition?.restEndpoint?.methods?.join(', ')}
</Badge>,
<div>/{endpointDefinition?.restEndpoint?.url ?? 'N/A'}</div>,
];
})}
/>
)}
{filteredEndpoints.length === 0 && (
<IndicatorCard
showIcon
children={
<div> <div>
{endpointDefinition?.exists ? ( No REST endpoints can be created for this table
<Link <LearnMoreLink href="https://hasura.io/docs/latest/restified/overview" />
to={{ </div>
pathname: `/api/rest/details/${endpointDefinition.restEndpoint?.name}`, }
state: { status="negative"
...endpointDefinition.restEndpoint, customIcon={FaExclamation}
currentQuery: endpointDefinition.query.query, />
}, )}
}} {filteredEndpoints.length > 0 && (
> <IndicatorCard
{method.label}{' '} showIcon
<FaExternalLinkAlt className="relative ml-1 -top-0.5" /> children={
</Link> <div>
) : ( Creating REST Endpoints will add metadata entries to your Hasura
method.label project
)} <LearnMoreLink href="https://hasura.io/docs/latest/restified/overview" />
</div>, </div>
<Badge color={method.color}> }
{endpointDefinition?.restEndpoint?.methods?.join(', ')} status="info"
</Badge>, customIcon={FaExclamation}
<div>/{endpointDefinition?.restEndpoint?.url ?? 'N/A'}</div>, />
]; )}
})}
/>
<IndicatorCard
showIcon
children={
<div>
Creating REST Endpoints will add metadata entries to your Hasura
project
<LearnMoreLink href="https://hasura.io/docs/latest/restified/overview" />
</div>
}
status="info"
customIcon={FaExclamation}
/>
</div> </div>
</Dialog> </Dialog>
); );

View File

@ -295,8 +295,8 @@ export const getValueWithType = (variableData: VariableState) => {
// NOTE: bool_exp are of JSON type, so pass it as JSON object (issue: https://github.com/hasura/graphql-engine/issues/9671) // NOTE: bool_exp are of JSON type, so pass it as JSON object (issue: https://github.com/hasura/graphql-engine/issues/9671)
if ( if (
(variableData.type.endsWith('_exp') || (variableData.type.includes('_exp') ||
variableData.type.endsWith('_input')) && variableData.type.includes('_input')) &&
isJsonString(variableData.value) isJsonString(variableData.value)
) { ) {
return JSON.parse(variableData.value); return JSON.parse(variableData.value);

View File

@ -9,7 +9,14 @@ export const dataInitialData: Partial<Metadata['metadata']> = {
{ {
name: 'default', name: 'default',
kind: 'postgres', kind: 'postgres',
tables: [], tables: [
{
table: {
name: 'user',
schema: 'public',
},
},
],
configuration: { configuration: {
connection_info: { connection_info: {
database_url: { database_url: {

View File

@ -1,7 +1,12 @@
import { Microfiber } from 'microfiber'; import { Microfiber } from 'microfiber';
import { useIntrospectionSchema } from '../../../components/Services/Actions/Common/components/ImportTypesModal/useIntrospectionSchema'; import { useIntrospectionSchema } from '../../../components/Services/Actions/Common/components/ImportTypesModal/useIntrospectionSchema';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { Query, RestEndpoint } from '../../hasura-metadata-types'; import {
MetadataTable,
Query,
RestEndpoint,
Source,
} from '../../hasura-metadata-types';
import { import {
Operation, Operation,
generateDeleteEndpoint, generateDeleteEndpoint,
@ -11,7 +16,7 @@ import {
generateViewEndpoint, generateViewEndpoint,
} from './utils'; } from './utils';
import { formatSdl } from 'format-graphql'; import { formatSdl } from 'format-graphql';
import { useMetadata } from '../../MetadataAPI'; import { useMetadata } from '../../hasura-metadata-api';
export type EndpointType = 'READ' | 'READ_ALL' | 'CREATE' | 'UPDATE' | 'DELETE'; export type EndpointType = 'READ' | 'READ_ALL' | 'CREATE' | 'UPDATE' | 'DELETE';
@ -26,8 +31,10 @@ type EndpointDefinitions = {
>; >;
}; };
type Table = MetadataTable & { table: { name: string; schema: string } };
export type Generator = { export type Generator = {
regExp: RegExp; operationName: (source: Source, table: Table) => string;
generator: ( generator: (
root: string, root: string,
table: string, table: string,
@ -36,6 +43,21 @@ export type Generator = {
) => EndpointDefinition; ) => EndpointDefinition;
}; };
export const getSchemaPrefix = (source: Source, table: Table) => {
const schemaName = table.table.schema;
if (source.kind === 'mssql' && schemaName === 'dbo') {
return '';
}
if (
['cockroach', 'postgres', 'citus'].includes(source.kind) &&
schemaName === 'public'
) {
return '';
}
return `${table?.table?.schema}_`;
};
export const getOperations = (microfiber: any) => { export const getOperations = (microfiber: any) => {
const queryType = microfiber.getQueryType(); const queryType = microfiber.getQueryType();
const mutationType = microfiber.getMutationType(); const mutationType = microfiber.getMutationType();
@ -90,24 +112,59 @@ export const getOperations = (microfiber: any) => {
const generators: Record<EndpointType, Generator> = { const generators: Record<EndpointType, Generator> = {
READ: { READ: {
regExp: /fetch data from the table: "(.+)" using primary key columns$/, operationName: (source, table) => {
if (table?.configuration?.custom_root_fields?.select_by_pk) {
return table?.configuration?.custom_root_fields?.select_by_pk;
}
const schemaPrefix = getSchemaPrefix(source, table);
const tableName = table.configuration?.custom_name ?? table?.table?.name;
return `${schemaPrefix}${tableName}_by_pk`;
},
generator: generateViewEndpoint, generator: generateViewEndpoint,
}, },
READ_ALL: { READ_ALL: {
regExp: /fetch data from the table: "(.+)"$/, operationName: (source, table) => {
if (table?.configuration?.custom_root_fields?.select) {
return table?.configuration?.custom_root_fields?.select;
}
const schemaPrefix = getSchemaPrefix(source, table);
const tableName = table.configuration?.custom_name ?? table?.table?.name;
return `${schemaPrefix}${tableName}`;
},
generator: generateViewAllEndpoint, generator: generateViewAllEndpoint,
}, },
CREATE: { CREATE: {
regExp: /insert a single row into the table: "(.+)"$/, operationName: (source, table) => {
if (table?.configuration?.custom_root_fields?.insert_one) {
return table?.configuration?.custom_root_fields?.insert_one;
}
const schemaPrefix = getSchemaPrefix(source, table);
const tableName = table.configuration?.custom_name ?? table?.table?.name;
return `insert_${schemaPrefix}${tableName}_one`;
},
generator: generateInsertEndpoint, generator: generateInsertEndpoint,
}, },
UPDATE: { UPDATE: {
regExp: /update single row of the table: "(.+)"$/, operationName: (source, table) => {
if (table?.configuration?.custom_root_fields?.update_by_pk) {
return table?.configuration?.custom_root_fields?.update_by_pk;
}
const schemaPrefix = getSchemaPrefix(source, table);
const tableName = table.configuration?.custom_name ?? table?.table?.name;
return `update_${schemaPrefix}${tableName}_by_pk`;
},
generator: generateUpdateEndpoint, generator: generateUpdateEndpoint,
}, },
DELETE: { DELETE: {
regExp: /delete single row from the table: "(.+)"$/, operationName: (source, table) => {
if (table?.configuration?.custom_root_fields?.delete_by_pk) {
return table?.configuration?.custom_root_fields?.delete_by_pk;
}
const schemaPrefix = getSchemaPrefix(source, table);
const tableName = table.configuration?.custom_name ?? table?.table?.name;
return `delete_${schemaPrefix}${tableName}_by_pk`;
},
generator: generateDeleteEndpoint, generator: generateDeleteEndpoint,
}, },
}; };
@ -119,12 +176,15 @@ export const useRestEndpointDefinitions = () => {
error, error,
} = useIntrospectionSchema(); } = useIntrospectionSchema();
const { data: metadata } = useMetadata(); const { data: metadata } = useMetadata(m => ({
restEndpoints: m.metadata?.rest_endpoints,
sources: m.metadata?.sources,
}));
const [data, setData] = useState<EndpointDefinitions>(); const [data, setData] = useState<EndpointDefinitions>();
useEffect(() => { useEffect(() => {
const existingRestEndpoints = metadata?.metadata?.rest_endpoints || []; const existingRestEndpoints = metadata?.restEndpoints || [];
if (introspectionSchema) { if (introspectionSchema) {
const response: EndpointDefinitions = {}; const response: EndpointDefinitions = {};
@ -137,25 +197,40 @@ export const useRestEndpointDefinitions = () => {
return; return;
} }
for (const operation of operations.operations) { for (const source of metadata?.sources || []) {
for (const endpointType in generators) { const sourcePrefix = source.customization?.root_fields?.prefix || '';
const match = operation.description?.match(
generators[endpointType as EndpointType].regExp
);
const table = match?.[1];
if (match) { const sourceSuffix = source.customization?.root_fields?.suffix || '';
const definition = generators[ for (const table of source.tables as Table[]) {
endpointType as EndpointType for (const [type, generator] of Object.entries(generators)) {
].generator(operations.root, table, operation, microfiber); const operationName = `${sourcePrefix}${generator.operationName(
source,
table
)}${sourceSuffix}`;
const operation = operations.operations.find(
operation => operation.name === operationName
);
if (!operation) {
continue;
}
const tableName = table?.table?.name;
const definition = generators[type as EndpointType].generator(
operations.root,
tableName,
operation,
microfiber
);
if (definition.query.query) { if (definition.query.query) {
definition.query.query = formatSdl(definition.query.query); definition.query.query = formatSdl(definition.query.query);
} }
response[table] = { response[tableName] = {
...(response[table] || {}), ...(response[tableName] || {}),
[endpointType]: { [type]: {
...definition, ...definition,
exists: existingRestEndpoints.some( exists: existingRestEndpoints.some(
endpoint => endpoint =>
@ -167,6 +242,7 @@ export const useRestEndpointDefinitions = () => {
} }
} }
} }
setData(response); setData(response);
} }
}, [introspectionSchema, metadata]); }, [introspectionSchema, metadata]);