mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-15 09:22:43 +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 (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
hasBackdrop
|
hasBackdrop
|
||||||
@ -86,13 +94,16 @@ export const RestEndpointModal = (props: RestEndpointModalProps) => {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="p-4 flex flex-col gap-4">
|
<div className="p-4 flex flex-col gap-4">
|
||||||
|
{filteredEndpoints.length > 0 && (
|
||||||
<CardedTable
|
<CardedTable
|
||||||
columns={[
|
columns={[
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={selectedMethods.length === ENDPOINTS.length}
|
checked={selectedMethods.length === ENDPOINTS.length}
|
||||||
onCheckedChange={checked => {
|
onCheckedChange={checked => {
|
||||||
if (checked) {
|
if (checked) {
|
||||||
setSelectedMethods(ENDPOINTS.map(endpoint => endpoint.value));
|
setSelectedMethods(
|
||||||
|
ENDPOINTS.map(endpoint => endpoint.value)
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
setSelectedMethods([]);
|
setSelectedMethods([]);
|
||||||
}
|
}
|
||||||
@ -102,13 +113,15 @@ export const RestEndpointModal = (props: RestEndpointModalProps) => {
|
|||||||
'METHOD',
|
'METHOD',
|
||||||
'PATH',
|
'PATH',
|
||||||
]}
|
]}
|
||||||
data={ENDPOINTS.map(method => {
|
data={filteredEndpoints.map(method => {
|
||||||
const endpointDefinition =
|
const endpointDefinition =
|
||||||
tableEndpointDefinitions[method.value as EndpointType];
|
tableEndpointDefinitions[method.value as EndpointType];
|
||||||
|
|
||||||
return [
|
return [
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={selectedMethods.includes(method.value as EndpointType)}
|
checked={selectedMethods.includes(
|
||||||
|
method.value as EndpointType
|
||||||
|
)}
|
||||||
disabled={endpointDefinition?.exists}
|
disabled={endpointDefinition?.exists}
|
||||||
onCheckedChange={checked => {
|
onCheckedChange={checked => {
|
||||||
if (checked) {
|
if (checked) {
|
||||||
@ -150,6 +163,21 @@ export const RestEndpointModal = (props: RestEndpointModalProps) => {
|
|||||||
];
|
];
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
{filteredEndpoints.length === 0 && (
|
||||||
|
<IndicatorCard
|
||||||
|
showIcon
|
||||||
|
children={
|
||||||
|
<div>
|
||||||
|
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
|
<IndicatorCard
|
||||||
showIcon
|
showIcon
|
||||||
children={
|
children={
|
||||||
@ -162,6 +190,7 @@ export const RestEndpointModal = (props: RestEndpointModalProps) => {
|
|||||||
status="info"
|
status="info"
|
||||||
customIcon={FaExclamation}
|
customIcon={FaExclamation}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Dialog>
|
</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)
|
// 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);
|
||||||
|
@ -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: {
|
||||||
|
@ -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]);
|
||||||
|
Loading…
Reference in New Issue
Block a user