From a9468f620d73af35739d547f00f51e6ceba883a3 Mon Sep 17 00:00:00 2001 From: Daniele Cammareri Date: Wed, 19 Jul 2023 10:45:56 +0200 Subject: [PATCH] fix: mssql support for create rest endpoint feature PR-URL: https://github.com/hasura/graphql-engine-mono/pull/9876 GitOrigin-RevId: f3a027a872e70c8c94b6b6b081644c30bcfc72cb --- .../RestEndpointModal/RestEndpointModal.tsx | 163 +++++++++++------- .../Services/ApiExplorer/Rest/utils.ts | 4 +- .../lib/features/Data/mocks/metadata.mocks.ts | 9 +- .../hooks/useRestEndpointDefinitions.ts | 122 ++++++++++--- 4 files changed, 205 insertions(+), 93 deletions(-) diff --git a/frontend/libs/console/legacy-ce/src/lib/components/Services/ApiExplorer/Rest/RestEndpointModal/RestEndpointModal.tsx b/frontend/libs/console/legacy-ce/src/lib/components/Services/ApiExplorer/Rest/RestEndpointModal/RestEndpointModal.tsx index 7a5b8e72e1b..f1cc7b973f2 100644 --- a/frontend/libs/console/legacy-ce/src/lib/components/Services/ApiExplorer/Rest/RestEndpointModal/RestEndpointModal.tsx +++ b/frontend/libs/console/legacy-ce/src/lib/components/Services/ApiExplorer/Rest/RestEndpointModal/RestEndpointModal.tsx @@ -38,6 +38,14 @@ export const RestEndpointModal = (props: RestEndpointModalProps) => { [] ); + const filteredEndpoints = React.useMemo( + () => + ENDPOINTS.filter( + method => !!tableEndpointDefinitions[method.value as EndpointType] + ), + [tableEndpointDefinitions] + ); + return ( { } >
- { - 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 && ( + { 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 [ + { + if (checked) { + setSelectedMethods([ + ...selectedMethods, + method.value as EndpointType, + ]); + } else { + setSelectedMethods( + selectedMethods.filter( + selectedMethod => selectedMethod !== method.value + ) + ); + } + }} + />, +
+ {endpointDefinition?.exists ? ( + + {method.label}{' '} + + + ) : ( + method.label + )} +
, + + {endpointDefinition?.restEndpoint?.methods?.join(', ')} + , +
/{endpointDefinition?.restEndpoint?.url ?? 'N/A'}
, + ]; + })} + /> + )} + {filteredEndpoints.length === 0 && ( + - {endpointDefinition?.exists ? ( - - {method.label}{' '} - - - ) : ( - method.label - )} -
, - - {endpointDefinition?.restEndpoint?.methods?.join(', ')} - , -
/{endpointDefinition?.restEndpoint?.url ?? 'N/A'}
, - ]; - })} - /> - - Creating REST Endpoints will add metadata entries to your Hasura - project - - - } - status="info" - customIcon={FaExclamation} - /> + No REST endpoints can be created for this table + + + } + status="negative" + customIcon={FaExclamation} + /> + )} + {filteredEndpoints.length > 0 && ( + + Creating REST Endpoints will add metadata entries to your Hasura + project + + + } + status="info" + customIcon={FaExclamation} + /> + )}
); diff --git a/frontend/libs/console/legacy-ce/src/lib/components/Services/ApiExplorer/Rest/utils.ts b/frontend/libs/console/legacy-ce/src/lib/components/Services/ApiExplorer/Rest/utils.ts index 8d1ed62503c..1b83db28210 100644 --- a/frontend/libs/console/legacy-ce/src/lib/components/Services/ApiExplorer/Rest/utils.ts +++ b/frontend/libs/console/legacy-ce/src/lib/components/Services/ApiExplorer/Rest/utils.ts @@ -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); diff --git a/frontend/libs/console/legacy-ce/src/lib/features/Data/mocks/metadata.mocks.ts b/frontend/libs/console/legacy-ce/src/lib/features/Data/mocks/metadata.mocks.ts index b3bbe811cfc..5f2a59c2536 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/Data/mocks/metadata.mocks.ts +++ b/frontend/libs/console/legacy-ce/src/lib/features/Data/mocks/metadata.mocks.ts @@ -9,7 +9,14 @@ export const dataInitialData: Partial = { { name: 'default', kind: 'postgres', - tables: [], + tables: [ + { + table: { + name: 'user', + schema: 'public', + }, + }, + ], configuration: { connection_info: { database_url: { diff --git a/frontend/libs/console/legacy-ce/src/lib/features/RestEndpoints/hooks/useRestEndpointDefinitions.ts b/frontend/libs/console/legacy-ce/src/lib/features/RestEndpoints/hooks/useRestEndpointDefinitions.ts index 564808cb03b..d00c2201dad 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/RestEndpoints/hooks/useRestEndpointDefinitions.ts +++ b/frontend/libs/console/legacy-ce/src/lib/features/RestEndpoints/hooks/useRestEndpointDefinitions.ts @@ -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 = { 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(); 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]);