mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-14 17:02:49 +03:00
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:
parent
4f6ef10e17
commit
a9468f620d
@ -38,6 +38,14 @@ export const RestEndpointModal = (props: RestEndpointModalProps) => {
|
||||
[]
|
||||
);
|
||||
|
||||
const filteredEndpoints = React.useMemo(
|
||||
() =>
|
||||
ENDPOINTS.filter(
|
||||
method => !!tableEndpointDefinitions[method.value as EndpointType]
|
||||
),
|
||||
[tableEndpointDefinitions]
|
||||
);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
hasBackdrop
|
||||
@ -86,82 +94,103 @@ export const RestEndpointModal = (props: RestEndpointModalProps) => {
|
||||
}
|
||||
>
|
||||
<div className="p-4 flex flex-col gap-4">
|
||||
<CardedTable
|
||||
columns={[
|
||||
<Checkbox
|
||||
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 [
|
||||
{filteredEndpoints.length > 0 && (
|
||||
<CardedTable
|
||||
columns={[
|
||||
<Checkbox
|
||||
checked={selectedMethods.includes(method.value as EndpointType)}
|
||||
disabled={endpointDefinition?.exists}
|
||||
checked={selectedMethods.length === ENDPOINTS.length}
|
||||
onCheckedChange={checked => {
|
||||
if (checked) {
|
||||
setSelectedMethods([
|
||||
...selectedMethods,
|
||||
method.value as EndpointType,
|
||||
]);
|
||||
} else {
|
||||
setSelectedMethods(
|
||||
selectedMethods.filter(
|
||||
selectedMethod => selectedMethod !== method.value
|
||||
)
|
||||
ENDPOINTS.map(endpoint => endpoint.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>
|
||||
{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>,
|
||||
];
|
||||
})}
|
||||
/>
|
||||
<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}
|
||||
/>
|
||||
No REST endpoints can be created for this table
|
||||
<LearnMoreLink href="https://hasura.io/docs/latest/restified/overview" />
|
||||
</div>
|
||||
}
|
||||
status="negative"
|
||||
customIcon={FaExclamation}
|
||||
/>
|
||||
)}
|
||||
{filteredEndpoints.length > 0 && (
|
||||
<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>
|
||||
</Dialog>
|
||||
);
|
||||
|
@ -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)
|
||||
if (
|
||||
(variableData.type.endsWith('_exp') ||
|
||||
variableData.type.endsWith('_input')) &&
|
||||
(variableData.type.includes('_exp') ||
|
||||
variableData.type.includes('_input')) &&
|
||||
isJsonString(variableData.value)
|
||||
) {
|
||||
return JSON.parse(variableData.value);
|
||||
|
@ -9,7 +9,14 @@ export const dataInitialData: Partial<Metadata['metadata']> = {
|
||||
{
|
||||
name: 'default',
|
||||
kind: 'postgres',
|
||||
tables: [],
|
||||
tables: [
|
||||
{
|
||||
table: {
|
||||
name: 'user',
|
||||
schema: 'public',
|
||||
},
|
||||
},
|
||||
],
|
||||
configuration: {
|
||||
connection_info: {
|
||||
database_url: {
|
||||
|
@ -1,7 +1,12 @@
|
||||
import { Microfiber } from 'microfiber';
|
||||
import { useIntrospectionSchema } from '../../../components/Services/Actions/Common/components/ImportTypesModal/useIntrospectionSchema';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Query, RestEndpoint } from '../../hasura-metadata-types';
|
||||
import {
|
||||
MetadataTable,
|
||||
Query,
|
||||
RestEndpoint,
|
||||
Source,
|
||||
} from '../../hasura-metadata-types';
|
||||
import {
|
||||
Operation,
|
||||
generateDeleteEndpoint,
|
||||
@ -11,7 +16,7 @@ import {
|
||||
generateViewEndpoint,
|
||||
} from './utils';
|
||||
import { formatSdl } from 'format-graphql';
|
||||
import { useMetadata } from '../../MetadataAPI';
|
||||
import { useMetadata } from '../../hasura-metadata-api';
|
||||
|
||||
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 = {
|
||||
regExp: RegExp;
|
||||
operationName: (source: Source, table: Table) => string;
|
||||
generator: (
|
||||
root: string,
|
||||
table: string,
|
||||
@ -36,6 +43,21 @@ export type Generator = {
|
||||
) => 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) => {
|
||||
const queryType = microfiber.getQueryType();
|
||||
const mutationType = microfiber.getMutationType();
|
||||
@ -90,24 +112,59 @@ export const getOperations = (microfiber: any) => {
|
||||
|
||||
const generators: Record<EndpointType, Generator> = {
|
||||
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,
|
||||
},
|
||||
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,
|
||||
},
|
||||
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,
|
||||
},
|
||||
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,
|
||||
},
|
||||
|
||||
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,
|
||||
},
|
||||
};
|
||||
@ -119,12 +176,15 @@ export const useRestEndpointDefinitions = () => {
|
||||
error,
|
||||
} = useIntrospectionSchema();
|
||||
|
||||
const { data: metadata } = useMetadata();
|
||||
const { data: metadata } = useMetadata(m => ({
|
||||
restEndpoints: m.metadata?.rest_endpoints,
|
||||
sources: m.metadata?.sources,
|
||||
}));
|
||||
|
||||
const [data, setData] = useState<EndpointDefinitions>();
|
||||
|
||||
useEffect(() => {
|
||||
const existingRestEndpoints = metadata?.metadata?.rest_endpoints || [];
|
||||
const existingRestEndpoints = metadata?.restEndpoints || [];
|
||||
|
||||
if (introspectionSchema) {
|
||||
const response: EndpointDefinitions = {};
|
||||
@ -137,25 +197,40 @@ export const useRestEndpointDefinitions = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const operation of operations.operations) {
|
||||
for (const endpointType in generators) {
|
||||
const match = operation.description?.match(
|
||||
generators[endpointType as EndpointType].regExp
|
||||
);
|
||||
const table = match?.[1];
|
||||
for (const source of metadata?.sources || []) {
|
||||
const sourcePrefix = source.customization?.root_fields?.prefix || '';
|
||||
|
||||
if (match) {
|
||||
const definition = generators[
|
||||
endpointType as EndpointType
|
||||
].generator(operations.root, table, operation, microfiber);
|
||||
const sourceSuffix = source.customization?.root_fields?.suffix || '';
|
||||
for (const table of source.tables as Table[]) {
|
||||
for (const [type, generator] of Object.entries(generators)) {
|
||||
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) {
|
||||
definition.query.query = formatSdl(definition.query.query);
|
||||
}
|
||||
|
||||
response[table] = {
|
||||
...(response[table] || {}),
|
||||
[endpointType]: {
|
||||
response[tableName] = {
|
||||
...(response[tableName] || {}),
|
||||
[type]: {
|
||||
...definition,
|
||||
exists: existingRestEndpoints.some(
|
||||
endpoint =>
|
||||
@ -167,6 +242,7 @@ export const useRestEndpointDefinitions = () => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setData(response);
|
||||
}
|
||||
}, [introspectionSchema, metadata]);
|
||||
|
Loading…
Reference in New Issue
Block a user