console: add support for application/x-www-form-urlencoded in rest connectors

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/4354
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Rikin Kachhia <54616969+rikinsk@users.noreply.github.com>
GitOrigin-RevId: 44dc48f3f226c57aac4393133245bf70d5f68acd
This commit is contained in:
Abhijeet Khangarot 2022-05-30 19:20:01 +05:30 committed by hasura-bot
parent 59df4dffb3
commit b09bb602bd
23 changed files with 713 additions and 329 deletions

View File

@ -46,6 +46,7 @@ is `graphql-default`, the field names generated will be `my_table`, `my_tableByP
- server: do not serialize env vars in logs or errors for remote schemas
- server: fix failure when executing consecutive delete mutations on mssql (#8462)
- server: bugfix: insertion of multiple empty objects should result in multiple entries (#8475)
- console: add support for application/x-www-form-urlencoded in rest connectors (#8097)
## v2.7.0

View File

@ -435,7 +435,7 @@ export const modifyV1ActionTransform = () => {
createV1ActionTransform('login');
cy.wait(AWAIT_SHORT);
// modify and save the action, the action should be converted into v2 and checkbox to remove body should be visible
// modify and save the action, the action should be converted into v2
cy.visit('/actions/manage/login/modify');
cy.url({ timeout: AWAIT_LONG }).should(
'eq',
@ -453,9 +453,6 @@ export const modifyV1ActionTransform = () => {
.should('be.visible')
.and('contain', 'Action saved successfully');
// check if checkbox to remove body is visible
cy.getBySel('transform-showRequestBody-checkbox').should('be.visible');
// delete the action
deleteActionTransform();
};

View File

@ -1,9 +1,14 @@
import React, { useState } from 'react';
import {
RequestTransformContentType,
RequestTransformMethod,
RequestTransformContentType,
} from '@/metadata/types';
import { KeyValuePair, RequestTransformState } from './stateDefaults';
import {
KeyValuePair,
RequestTransformState,
RequestTransformStateBody,
TransformationType,
} from './stateDefaults';
import RequestOptionsTransforms from './RequestOptionsTransforms';
import PayloadOptionsTransforms from './PayloadOptionsTransforms';
import SampleContextTransforms from './SampleContextTransforms';
@ -11,6 +16,7 @@ import Button from '../Button';
import AddIcon from '../Icons/Add';
type ConfigureTransformationProps = {
transformationType: TransformationType;
state: RequestTransformState;
resetSampleInput: () => void;
envVarsOnChange: (envVars: KeyValuePair[]) => void;
@ -19,10 +25,9 @@ type ConfigureTransformationProps = {
requestUrlOnChange: (requestUrl: string) => void;
requestQueryParamsOnChange: (requestQueryParams: KeyValuePair[]) => void;
requestAddHeadersOnChange: (requestAddHeaders: KeyValuePair[]) => void;
requestBodyOnChange: (requestBody: string) => void;
requestBodyEnabledOnChange: (enableRequestBody: boolean) => void;
requestBodyOnChange: (requestBody: RequestTransformStateBody) => void;
requestSampleInputOnChange: (requestSampleInput: string) => void;
requestContentTypeOnChange: (
requestContentTypeOnChange?: (
requestContentType: RequestTransformContentType
) => void;
requestUrlTransformOnChange: (data: boolean) => void;
@ -30,6 +35,7 @@ type ConfigureTransformationProps = {
};
const ConfigureTransformation: React.FC<ConfigureTransformationProps> = ({
transformationType,
state,
resetSampleInput,
envVarsOnChange,
@ -39,9 +45,7 @@ const ConfigureTransformation: React.FC<ConfigureTransformationProps> = ({
requestQueryParamsOnChange,
requestAddHeadersOnChange,
requestBodyOnChange,
requestBodyEnabledOnChange,
requestSampleInputOnChange,
requestContentTypeOnChange,
requestUrlTransformOnChange,
requestPayloadTransformOnChange,
}) => {
@ -58,8 +62,6 @@ const ConfigureTransformation: React.FC<ConfigureTransformationProps> = ({
requestBodyError,
requestSampleInput,
requestTransformedBody,
enableRequestBody,
requestContentType,
isRequestUrlTransform,
isRequestPayloadTransform,
} = state;
@ -105,6 +107,7 @@ const ConfigureTransformation: React.FC<ConfigureTransformationProps> = ({
{isContextAreaActive ? (
<SampleContextTransforms
transformationType={transformationType}
envVars={envVars}
sessionVars={sessionVars}
envVarsOnChange={envVarsOnChange}
@ -170,17 +173,14 @@ const ConfigureTransformation: React.FC<ConfigureTransformationProps> = ({
</Button>
{isRequestPayloadTransform ? (
<PayloadOptionsTransforms
transformationType={transformationType}
requestBody={requestBody}
requestBodyError={requestBodyError}
requestSampleInput={requestSampleInput}
requestTransformedBody={requestTransformedBody}
enableRequestBody={enableRequestBody}
requestContentType={requestContentType}
resetSampleInput={resetSampleInput}
requestBodyOnChange={requestBodyOnChange}
requestBodyEnabledOnChange={requestBodyEnabledOnChange}
requestSampleInputOnChange={requestSampleInputOnChange}
requestContentTypeOnChange={requestContentTypeOnChange}
/>
) : null}
</div>

View File

@ -4,12 +4,13 @@ import { Nullable } from '../../utils/tsUtils';
import { getAceCompleterFromString, editorDebounceTime } from '../utils';
import { useDebouncedEffect } from '../../../../hooks/useDebounceEffect';
import CrossIcon from '../../Icons/Cross';
import { RequestTransformStateBody } from '../stateDefaults';
type TemplateEditorProps = {
requestBody: string;
requestBody: RequestTransformStateBody;
requestBodyError: string;
requestSampleInput: string;
requestBodyOnChange: (requestBody: string) => void;
requestBodyOnChange: (requestBody: RequestTransformStateBody) => void;
height?: string;
width?: string;
};
@ -23,13 +24,15 @@ const TemplateEditor: React.FC<TemplateEditorProps> = ({
width,
}) => {
const editorRef = useRef<any>();
const [localValue, setLocalValue] = useState<string>(requestBody);
const [localValue, setLocalValue] = useState<string>(
requestBody.template ?? ''
);
const [localError, setLocalError] = useState<Nullable<string>>(
requestBodyError
);
useEffect(() => {
setLocalValue(requestBody);
setLocalValue(requestBody.template ?? '');
}, [requestBody]);
useEffect(() => {
@ -54,7 +57,7 @@ const TemplateEditor: React.FC<TemplateEditorProps> = ({
useDebouncedEffect(
() => {
requestBodyOnChange(localValue);
requestBodyOnChange({ ...requestBody, template: localValue });
},
editorDebounceTime,
[localValue]

View File

@ -1,48 +1,82 @@
import React, { useRef } from 'react';
import { RequestTransformBodyActions } from '@/metadata/types';
import { useDebouncedEffect } from '@/hooks/useDebounceEffect';
import { FaExclamationCircle } from 'react-icons/fa';
import CrossIcon from '../Icons/Cross';
import TemplateEditor from './CustomEditors/TemplateEditor';
import JsonEditor from './CustomEditors/JsonEditor';
import AceEditor from '../AceEditor/BaseEditor';
import { RequestTransformContentType } from '../../../metadata/types';
import ResetIcon from '../Icons/Reset';
import { buttonShadow, focusYellowRing, inputStyles } from './utils';
import {
buttonShadow,
capitaliseFirstLetter,
editorDebounceTime,
focusYellowRing,
inputStyles,
} from './utils';
import NumberedSidebar from './CustomEditors/NumberedSidebar';
import {
KeyValuePair,
RequestTransformStateBody,
TransformationType,
} from './stateDefaults';
import KeyValueInput from './CustomEditors/KeyValueInput';
import { isEmpty } from '../utils/jsUtils';
import { requestBodyActionState } from './requestTransformState';
type PayloadOptionsTransformsProps = {
requestBody: string;
transformationType: TransformationType;
requestBody: RequestTransformStateBody;
requestBodyError: string;
requestSampleInput: string;
requestTransformedBody: string;
requestContentType: RequestTransformContentType;
enableRequestBody: boolean;
resetSampleInput: () => void;
requestBodyOnChange: (requestBody: string) => void;
requestBodyEnabledOnChange: (enableRequestBody: boolean) => void;
requestBodyOnChange: (requestBody: RequestTransformStateBody) => void;
requestSampleInputOnChange: (requestSampleInput: string) => void;
requestContentTypeOnChange: (
requestContentType: RequestTransformContentType
) => void;
};
const PayloadOptionsTransforms: React.FC<PayloadOptionsTransformsProps> = ({
transformationType,
requestBody,
requestBodyError,
requestSampleInput,
requestTransformedBody,
requestContentType,
enableRequestBody,
resetSampleInput,
requestBodyOnChange,
requestBodyEnabledOnChange,
requestSampleInputOnChange,
requestContentTypeOnChange,
}) => {
const editorRef = useRef<any>();
const showRequestContentTypeOptions = false;
const requestContentTypeOptions = [
'application/json',
'application/x-www-form-urlencoded',
const requestBodyTypeOptions = [
{
value: requestBodyActionState.remove,
text: 'disabled',
},
{
value: requestBodyActionState.transformApplicationJson,
text: 'application/json',
},
{
value: requestBodyActionState.transformFormUrlEncoded,
text: 'application/x-www-form-urlencoded',
},
];
const [localFormElements, setLocalFormElements] = React.useState<
KeyValuePair[]
>(requestBody.form_template ?? [{ name: '', value: '' }]);
React.useEffect(() => {
setLocalFormElements(
requestBody.form_template ?? [{ name: '', value: '' }]
);
}, [requestBody]);
useDebouncedEffect(
() => {
requestBodyOnChange({ ...requestBody, form_template: localFormElements });
},
editorDebounceTime,
[localFormElements]
);
if (editorRef?.current?.editor?.renderer?.$cursorLayer?.element?.style) {
editorRef.current.editor.renderer.$cursorLayer.element.style.display =
@ -54,7 +88,9 @@ const PayloadOptionsTransforms: React.FC<PayloadOptionsTransformsProps> = ({
<div className="mb-md">
<NumberedSidebar
title="Sample Input"
description="Sample input defined by your Action Defintion."
description={`Sample input defined by your ${capitaliseFirstLetter(
transformationType
)} Defintion.`}
number="1"
>
<button
@ -81,71 +117,78 @@ const PayloadOptionsTransforms: React.FC<PayloadOptionsTransformsProps> = ({
<span>
The template which will transform your request body into the
required specification. You can use{' '}
<code className="text-xs">$body</code> to access the original
request body
<code className="text-xs">&#123;&#123;$body&#125;&#125;</code> to
access the original request body
</span>
}
number="2"
url="https://hasura.io/docs/latest/graphql/core/actions/transforms.html#request-body"
/>
<div className="mb-sm">
<input
checked={enableRequestBody}
id="request-enable"
name="request-body"
type="checkbox"
onChange={e => requestBodyEnabledOnChange(e.target.checked)}
className="rounded border-gray-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-yellow-400"
data-test="transform-showRequestBody-checkbox"
/>
<label className="ml-xs" htmlFor="request-enable">
Enable Request Body
</label>
</div>
{enableRequestBody ? (
>
<select
className={`ml-auto ${inputStyles}`}
value={requestBody.action}
onChange={e =>
requestBodyOnChange({
...requestBody,
action: e.target.value as RequestTransformBodyActions,
})
}
>
<option disabled>Request Body Type</option>
{requestBodyTypeOptions.map(option => (
<option key={option.value} value={option.value}>
{option.text}
</option>
))}
</select>
</NumberedSidebar>
{requestBody.action ===
requestBodyActionState.transformApplicationJson ? (
<TemplateEditor
requestBody={requestBody}
requestBodyError={requestBodyError}
requestSampleInput={requestSampleInput}
requestBodyOnChange={requestBodyOnChange}
/>
) : (
) : null}
{requestBody.action ===
requestBodyActionState.transformFormUrlEncoded ? (
<>
{!isEmpty(requestBodyError) && (
<div className="mb-sm" data-test="transform-requestBody-error">
<CrossIcon />
<span className="text-red-500 ml-sm">{requestBodyError}</span>
</div>
)}
<div className="grid gap-3 grid-cols-3 mb-sm">
<KeyValueInput
pairs={localFormElements}
setPairs={setLocalFormElements}
testId="add-url-encoded-body"
/>
</div>
</>
) : null}
{requestBody.action === requestBodyActionState.remove ? (
<div className="flex items-center text-gray-600 bg-gray-200 border border-gray-400 text-sm rounded p-sm">
<FaExclamationCircle className="mr-sm" />
The request body is disabled. No request body will be sent with this
action. Enable the request body to modify your request
{transformationType}. Enable the request body to modify your request
transformation.
</div>
)}
) : null}
</div>
{enableRequestBody ? (
{requestBody.action !== requestBodyActionState.remove ? (
<div className="mb-md">
<NumberedSidebar
title="Transformed Request Body"
description="Sample request body to be delivered based on your input and
transformation template."
number="3"
>
{showRequestContentTypeOptions ? (
<select
className={`ml-auto ${inputStyles}`}
value={requestContentType}
onChange={e =>
requestContentTypeOnChange(
e.target.value as RequestTransformContentType
)
}
>
<option disabled>Data Type</option>
{requestContentTypeOptions.map(option => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
) : null}
</NumberedSidebar>
/>
<AceEditor
mode="json"
editorRef={editorRef}

View File

@ -1,11 +1,12 @@
import React, { useState, useEffect } from 'react';
import { useDebouncedEffect } from '@/hooks/useDebounceEffect';
import { KeyValuePair } from './stateDefaults';
import { KeyValuePair, TransformationType } from './stateDefaults';
import KeyValueInput from './CustomEditors/KeyValueInput';
import NumberedSidebar from './CustomEditors/NumberedSidebar';
import { editorDebounceTime, setEnvVarsToLS } from './utils';
type SampleContextTransformsProps = {
transformationType: TransformationType;
envVars: KeyValuePair[];
sessionVars: KeyValuePair[];
envVarsOnChange: (envVars: KeyValuePair[]) => void;
@ -13,6 +14,7 @@ type SampleContextTransformsProps = {
};
const SampleContextTransforms: React.FC<SampleContextTransformsProps> = ({
transformationType,
envVars,
sessionVars,
envVarsOnChange,
@ -57,7 +59,8 @@ const SampleContextTransforms: React.FC<SampleContextTransformsProps> = ({
<span>
Enter a sample input for your provided env variables.
<br />
e.g. the sample value for ACTION_BASE_URL
e.g. the sample value for {transformationType.toUpperCase()}
_BASE_URL
</span>
}
number="1"

View File

@ -1,6 +1,7 @@
import {
RequestTransformMethod,
RequestTransformContentType,
RequestTransformBodyActions,
} from '../../../metadata/types';
import {
SET_ENV_VARS,
@ -15,7 +16,6 @@ import {
SET_REQUEST_BODY_ERROR,
SET_REQUEST_SAMPLE_INPUT,
SET_REQUEST_TRANSFORMED_BODY,
SET_ENABLE_REQUEST_BODY,
SET_REQUEST_CONTENT_TYPE,
SET_REQUEST_URL_TRANSFORM,
SET_REQUEST_PAYLOAD_TRANSFORM,
@ -32,7 +32,6 @@ import {
SetRequestBodyError,
SetRequestSampleInput,
SetRequestTransformedBody,
SetEnableRequestBody,
SetRequestTransformState,
SetRequestContentType,
SetRequestUrlTransform,
@ -46,6 +45,7 @@ import {
KeyValuePair,
defaultEventRequestBody,
defaultEventRequestSampleInput,
RequestTransformStateBody,
} from './stateDefaults';
import { getSessionVarsFromLS, getEnvVarsFromLS } from './utils';
@ -101,7 +101,9 @@ export const setRequestAddHeaders = (
requestAddHeaders,
});
export const setRequestBody = (requestBody: string): SetRequestBody => ({
export const setRequestBody = (
requestBody: RequestTransformStateBody
): SetRequestBody => ({
type: SET_REQUEST_BODY,
requestBody,
});
@ -127,13 +129,6 @@ export const setRequestTransformedBody = (
requestTransformedBody,
});
export const setEnableRequestBody = (
enableRequestBody: boolean
): SetEnableRequestBody => ({
type: SET_ENABLE_REQUEST_BODY,
enableRequestBody,
});
export const setRequestContentType = (
requestContentType: RequestTransformContentType
): SetRequestContentType => ({
@ -164,6 +159,12 @@ export const setRequestTransformState = (
const currentVersion = 2;
export const requestBodyActionState = {
remove: 'remove' as RequestTransformBodyActions,
transformApplicationJson: 'transform' as RequestTransformBodyActions,
transformFormUrlEncoded: 'x_www_form_urlencoded' as RequestTransformBodyActions,
};
export const requestTransformState: RequestTransformState = {
version: currentVersion,
envVars: [],
@ -174,11 +175,10 @@ export const requestTransformState: RequestTransformState = {
requestUrlPreview: '',
requestQueryParams: [],
requestAddHeaders: [],
requestBody: '',
requestBody: { action: requestBodyActionState.transformApplicationJson },
requestBodyError: '',
requestSampleInput: '',
requestTransformedBody: '',
enableRequestBody: true,
requestContentType: defaultRequestContentType,
isRequestUrlTransform: false,
isRequestPayloadTransform: false,
@ -192,7 +192,11 @@ export const getActionRequestTransformDefaultState = (): RequestTransformState =
sessionVars: getSessionVarsFromLS(),
requestQueryParams: [{ name: '', value: '' }],
requestAddHeaders: [{ name: '', value: '' }],
requestBody: defaultActionRequestBody,
requestBody: {
action: requestBodyActionState.transformApplicationJson,
template: defaultActionRequestBody,
form_template: [{ name: 'name', value: '{{$body.action.name}}' }],
},
requestSampleInput: defaultActionRequestSampleInput,
};
};
@ -204,7 +208,11 @@ export const getEventRequestTransformDefaultState = (): RequestTransformState =>
sessionVars: getSessionVarsFromLS(),
requestQueryParams: [{ name: '', value: '' }],
requestAddHeaders: [{ name: '', value: '' }],
requestBody: defaultEventRequestBody,
requestBody: {
action: requestBodyActionState.transformApplicationJson,
template: defaultEventRequestBody,
form_template: [{ name: 'name', value: '{{$body.table.name}}' }],
},
requestSampleInput: defaultEventRequestSampleInput,
};
};
@ -274,11 +282,6 @@ export const requestTransformReducer = (
...state,
requestTransformedBody: action.requestTransformedBody,
};
case SET_ENABLE_REQUEST_BODY:
return {
...state,
enableRequestBody: action.enableRequestBody,
};
case SET_REQUEST_CONTENT_TYPE:
return {
...state,

View File

@ -9,6 +9,7 @@ import {
RequestTransformMethod,
RequestTransformContentType,
RequestTransformTemplateEngine,
RequestTransformBody,
} from '../../../metadata/types';
import { Nullable } from '../utils/tsUtils';
@ -82,7 +83,7 @@ export interface SetRequestAddHeaders extends ReduxAction {
export interface SetRequestBody extends ReduxAction {
type: typeof SET_REQUEST_BODY;
requestBody: string;
requestBody: RequestTransformStateBody;
}
export interface SetRequestBodyError extends ReduxAction {
@ -100,11 +101,6 @@ export interface SetRequestTransformedBody extends ReduxAction {
requestTransformedBody: string;
}
export interface SetEnableRequestBody extends ReduxAction {
type: typeof SET_ENABLE_REQUEST_BODY;
enableRequestBody: boolean;
}
export interface SetRequestContentType extends ReduxAction {
type: typeof SET_REQUEST_CONTENT_TYPE;
requestContentType: RequestTransformContentType;
@ -138,7 +134,6 @@ export type RequestTransformEvents =
| SetRequestBodyError
| SetRequestSampleInput
| SetRequestTransformedBody
| SetEnableRequestBody
| SetRequestContentType
| SetRequestUrlTransform
| SetRequestPayloadTransform
@ -154,17 +149,21 @@ export type RequestTransformState = {
requestUrlPreview: string;
requestQueryParams: KeyValuePair[];
requestAddHeaders: KeyValuePair[];
requestBody: string;
requestBody: RequestTransformStateBody;
requestBodyError: string;
requestSampleInput: string;
requestTransformedBody: string;
enableRequestBody: boolean;
requestContentType: RequestTransformContentType;
isRequestUrlTransform: boolean;
isRequestPayloadTransform: boolean;
templatingEngine: RequestTransformTemplateEngine;
};
export type RequestTransformStateBody = Omit<
RequestTransformBody,
'form_template'
> & { form_template?: KeyValuePair[] };
export type KeyValuePair = {
name: string;
value: string;
@ -178,6 +177,8 @@ export type GraphiQlHeader = {
isDisabled: boolean;
};
export type TransformationType = 'action' | 'event';
export const defaultActionRequestSampleInput = getActionRequestSampleInput(
defaultActionDefSdl,
defaultTypesDefSdl

View File

@ -1,15 +1,23 @@
import { RequestTransform, RequestTransformMethod } from '@/metadata/types';
import {
RequestTransform,
RequestTransformBody,
RequestTransformMethod,
} from '@/metadata/types';
import { getLSItem, setLSItem, LS_KEYS } from '@/utils/localStorage';
import {
defaultRequestContentType,
GraphiQlHeader,
KeyValuePair,
RequestTransformState,
RequestTransformStateBody,
} from './stateDefaults';
import { isEmpty, isJsonString } from '../utils/jsUtils';
import { Nullable } from '../utils/tsUtils';
import { requestBodyActionState } from './requestTransformState';
export const getPairsObjFromArray = (pairs: KeyValuePair[]) => {
export const getPairsObjFromArray = (
pairs: KeyValuePair[]
): Record<string, string> => {
let obj = {};
pairs.forEach(({ name, value }) => {
@ -101,12 +109,22 @@ const getUrlWithBasePrefix = (val?: string) => {
return val ? `{{$base_url}}${val}` : undefined;
};
const getTransformBodyServer = (reqBody: RequestTransformStateBody) => {
if (reqBody.action === requestBodyActionState.remove)
return { action: reqBody.action };
else if (reqBody.action === requestBodyActionState.transformApplicationJson)
return { action: reqBody.action, template: reqBody.template };
return {
action: reqBody.action,
form_template: getPairsObjFromArray(reqBody.form_template ?? []),
};
};
export const getRequestTransformObject = (
transformState: RequestTransformState
) => {
const isRequestUrlTransform = transformState.isRequestUrlTransform;
const isRequestPayloadTransform = transformState.isRequestPayloadTransform;
const enableRequestBody = transformState.enableRequestBody;
if (!isRequestUrlTransform && !isRequestPayloadTransform) return null;
@ -135,16 +153,22 @@ export const getRequestTransformObject = (
if (isRequestPayloadTransform) {
obj = {
...obj,
body: enableRequestBody
? {
action: 'transform',
template: checkEmptyString(transformState.requestBody),
}
: {
action: 'remove',
},
content_type: transformState.requestContentType,
body: getTransformBodyServer(transformState.requestBody),
};
if (
transformState.requestBody.action ===
requestBodyActionState.transformFormUrlEncoded
) {
obj = {
...obj,
request_headers: {
remove_headers: ['content-type'],
add_headers: {
'content-type': 'application/x-www-form-urlencoded',
},
},
};
}
}
return obj;
@ -209,14 +233,14 @@ type RequestTransformerV1 = RequestTransformerFields & {
type RequestTransformerV2 = RequestTransformerFields & {
version: 2;
body?: Record<string, string>;
body?: RequestTransformBody;
};
type RequestTransformer = RequestTransformerV1 | RequestTransformerV2;
const getTransformer = (
version: 1 | 2,
transformerBody?: string,
transformerBody?: RequestTransformStateBody,
transformerUrl?: string,
requestMethod?: Nullable<RequestTransformMethod>,
queryParams?: KeyValuePair[]
@ -224,7 +248,7 @@ const getTransformer = (
return version === 1
? {
version,
body: checkEmptyString(transformerBody),
body: checkEmptyString(transformerBody?.template ?? ''),
url: checkEmptyString(transformerUrl),
method: requestMethod,
query_params: queryParams
@ -234,10 +258,9 @@ const getTransformer = (
}
: {
version,
body: {
action: 'transform',
template: checkEmptyString(transformerBody) ?? '{}',
},
body: transformerBody
? getTransformBodyServer(transformerBody)
: undefined,
url: checkEmptyString(transformerUrl),
method: requestMethod,
query_params: queryParams
@ -275,7 +298,7 @@ type ValidateTransformOptionsArgsType = {
webhookUrl: string;
envVarsFromContext?: KeyValuePair[];
sessionVarsFromContext?: KeyValuePair[];
transformerBody?: string;
transformerBody?: RequestTransformStateBody;
requestUrl?: string;
queryParams?: KeyValuePair[];
isEnvVar?: boolean;
@ -384,6 +407,28 @@ const getTrimmedRequestUrl = (val: string) => {
return val.startsWith(prefix) ? val.slice(prefix.length) : val;
};
const getRequestTransformBody = (
transform: RequestTransform
): RequestTransformStateBody => {
if (transform.body) {
return transform.version === 1
? {
action: requestBodyActionState.transformApplicationJson,
template: transform?.body ?? '',
}
: {
...transform.body,
form_template: getArrayFromServerPairObject(
transform.body?.form_template
),
};
}
return {
action: requestBodyActionState.transformApplicationJson,
template: '',
};
};
export const getTransformState = (
transform: RequestTransform,
sampleInput: string
@ -401,15 +446,10 @@ export const getTransformState = (
requestAddHeaders: getArrayFromServerPairObject(
transform?.request_headers?.add_headers
) ?? [{ name: '', value: '' }],
requestBody:
transform?.version === 1
? transform?.body ?? ''
: transform?.body?.template ?? '',
requestBody: getRequestTransformBody(transform),
requestBodyError: '',
requestSampleInput: sampleInput,
requestTransformedBody: '',
enableRequestBody:
transform?.version === 2 ? transform?.body?.action === 'transform' : true,
requestContentType: transform?.content_type ?? defaultRequestContentType,
isRequestUrlTransform:
!!transform?.method ||
@ -419,6 +459,9 @@ export const getTransformState = (
templatingEngine: transform?.template_engine ?? 'Kriti',
});
export const capitaliseFirstLetter = (val: string) =>
`${val[0].toUpperCase()}${val.slice(1)}`;
export const sidebarNumberStyles =
'-mb-9 -ml-14 bg-gray-50 text-sm font-medium border border-gray-400 rounded-full flex items-center justify-center h-lg w-lg';

View File

@ -24,7 +24,6 @@ import {
setRequestBodyError,
setRequestSampleInput,
setRequestTransformedBody,
setEnableRequestBody,
setRequestContentType,
setRequestUrlTransform,
setRequestPayloadTransform,
@ -33,7 +32,10 @@ import {
RequestTransformContentType,
RequestTransformMethod,
} from '@/metadata/types';
import { KeyValuePair } from '@/components/Common/ConfigureTransformation/stateDefaults';
import {
KeyValuePair,
RequestTransformStateBody,
} from '@/components/Common/ConfigureTransformation/stateDefaults';
import ConfigureTransformation from '@/components/Common/ConfigureTransformation/ConfigureTransformation';
import ActionEditor from '../Common/components/ActionEditor';
import Button from '../../../Common/Button';
@ -189,7 +191,7 @@ const AddAction: React.FC<AddActionProps> = ({
transformDispatch(setRequestAddHeaders(requestAddHeaders));
};
const requestBodyOnChange = (requestBody: string) => {
const requestBodyOnChange = (requestBody: RequestTransformStateBody) => {
transformDispatch(setRequestBody(requestBody));
};
@ -205,10 +207,6 @@ const AddAction: React.FC<AddActionProps> = ({
transformDispatch(setRequestTransformedBody(requestTransformedBody));
};
const requestBodyEnabledOnChange = (enableRequestBody: boolean) => {
transformDispatch(setEnableRequestBody(enableRequestBody));
};
const requestContentTypeOnChange = (
requestContentType: RequestTransformContentType
) => {
@ -356,6 +354,7 @@ const AddAction: React.FC<AddActionProps> = ({
/>
<ConfigureTransformation
transformationType="action"
state={transformState}
resetSampleInput={resetSampleInput}
envVarsOnChange={envVarsOnChange}
@ -365,7 +364,6 @@ const AddAction: React.FC<AddActionProps> = ({
requestQueryParamsOnChange={requestQueryParamsOnChange}
requestAddHeadersOnChange={requestAddHeadersOnChange}
requestBodyOnChange={requestBodyOnChange}
requestBodyEnabledOnChange={requestBodyEnabledOnChange}
requestSampleInputOnChange={requestSampleInputOnChange}
requestContentTypeOnChange={requestContentTypeOnChange}
requestUrlTransformOnChange={requestUrlTransformOnChange}

View File

@ -24,13 +24,15 @@ import {
setRequestBodyError,
setRequestSampleInput,
setRequestTransformedBody,
setEnableRequestBody,
setRequestContentType,
setRequestUrlTransform,
setRequestPayloadTransform,
setRequestTransformState,
} from '@/components/Common/ConfigureTransformation/requestTransformState';
import { KeyValuePair } from '@/components/Common/ConfigureTransformation/stateDefaults';
import {
KeyValuePair,
RequestTransformStateBody,
} from '@/components/Common/ConfigureTransformation/stateDefaults';
import {
RequestTransformContentType,
RequestTransformMethod,
@ -214,14 +216,10 @@ const ModifyAction: React.FC<ModifyProps> = ({
transformDispatch(setRequestAddHeaders(requestAddHeaders));
};
const requestBodyOnChange = (requestBody: string) => {
const requestBodyOnChange = (requestBody: RequestTransformStateBody) => {
transformDispatch(setRequestBody(requestBody));
};
const requestBodyEnabledOnChange = (enableRequestBody: boolean) => {
transformDispatch(setEnableRequestBody(enableRequestBody));
};
const requestBodyErrorOnChange = (requestBodyError: string) => {
transformDispatch(setRequestBodyError(requestBodyError));
};
@ -399,6 +397,7 @@ const ModifyAction: React.FC<ModifyProps> = ({
/>
<ConfigureTransformation
transformationType="action"
state={transformState}
resetSampleInput={resetSampleInput}
envVarsOnChange={envVarsOnChange}
@ -408,7 +407,6 @@ const ModifyAction: React.FC<ModifyProps> = ({
requestQueryParamsOnChange={requestQueryParamsOnChange}
requestAddHeadersOnChange={requestAddHeadersOnChange}
requestBodyOnChange={requestBodyOnChange}
requestBodyEnabledOnChange={requestBodyEnabledOnChange}
requestSampleInputOnChange={requestSampleInputOnChange}
requestContentTypeOnChange={requestContentTypeOnChange}
requestUrlTransformOnChange={requestUrlTransformOnChange}

View File

@ -16,7 +16,6 @@ import {
setRequestBodyError,
setRequestSampleInput,
setRequestTransformedBody,
setEnableRequestBody,
setRequestContentType,
setRequestUrlTransform,
setRequestPayloadTransform,
@ -25,7 +24,10 @@ import {
RequestTransformContentType,
RequestTransformMethod,
} from '@/metadata/types';
import { KeyValuePair } from '@/components/Common/ConfigureTransformation/stateDefaults';
import {
KeyValuePair,
RequestTransformStateBody,
} from '@/components/Common/ConfigureTransformation/stateDefaults';
import ConfigureTransformation from '@/components/Common/ConfigureTransformation/ConfigureTransformation';
import {
getValidateTransformOptions,
@ -184,14 +186,10 @@ const Add: React.FC<Props> = props => {
transformDispatch(setRequestAddHeaders(requestAddHeaders));
};
const requestBodyOnChange = (requestBody: string) => {
const requestBodyOnChange = (requestBody: RequestTransformStateBody) => {
transformDispatch(setRequestBody(requestBody));
};
const requestBodyEnabledOnChange = (enableRequestBody: boolean) => {
transformDispatch(setEnableRequestBody(enableRequestBody));
};
const requestBodyErrorOnChange = (requestBodyError: string) => {
transformDispatch(setRequestBodyError(requestBodyError));
};
@ -407,6 +405,7 @@ const Add: React.FC<Props> = props => {
handleToggleAllColumn={setState.toggleAllColumnChecked}
/>
<ConfigureTransformation
transformationType="event"
state={transformState}
resetSampleInput={resetSampleInput}
envVarsOnChange={envVarsOnChange}
@ -416,7 +415,6 @@ const Add: React.FC<Props> = props => {
requestQueryParamsOnChange={requestQueryParamsOnChange}
requestAddHeadersOnChange={requestAddHeadersOnChange}
requestBodyOnChange={requestBodyOnChange}
requestBodyEnabledOnChange={requestBodyEnabledOnChange}
requestSampleInputOnChange={requestSampleInputOnChange}
requestContentTypeOnChange={requestContentTypeOnChange}
requestUrlTransformOnChange={requestUrlTransformOnChange}

View File

@ -14,7 +14,6 @@ import {
setRequestBodyError,
setRequestSampleInput,
setRequestTransformedBody,
setEnableRequestBody,
setRequestContentType,
setRequestUrlTransform,
setRequestTransformState,
@ -25,7 +24,10 @@ import {
RequestTransformContentType,
RequestTransformMethod,
} from '@/metadata/types';
import { KeyValuePair } from '@/components/Common/ConfigureTransformation/stateDefaults';
import {
KeyValuePair,
RequestTransformStateBody,
} from '@/components/Common/ConfigureTransformation/stateDefaults';
import ConfigureTransformation from '@/components/Common/ConfigureTransformation/ConfigureTransformation';
import requestAction from '@/utils/requestAction';
import Endpoints from '@/Endpoints';
@ -162,14 +164,10 @@ const Modify: React.FC<Props> = props => {
transformDispatch(setRequestAddHeaders(requestAddHeaders));
};
const requestBodyOnChange = (requestBody: string) => {
const requestBodyOnChange = (requestBody: RequestTransformStateBody) => {
transformDispatch(setRequestBody(requestBody));
};
const requestBodyEnabledOnChange = (enableRequestBody: boolean) => {
transformDispatch(setEnableRequestBody(enableRequestBody));
};
const requestBodyErrorOnChange = (requestBodyError: string) => {
transformDispatch(setRequestBodyError(requestBodyError));
};
@ -379,6 +377,7 @@ const Modify: React.FC<Props> = props => {
save={saveWrapper('headers')}
/>
<ConfigureTransformation
transformationType="event"
state={transformState}
resetSampleInput={resetSampleInput}
envVarsOnChange={envVarsOnChange}
@ -388,7 +387,6 @@ const Modify: React.FC<Props> = props => {
requestQueryParamsOnChange={requestQueryParamsOnChange}
requestAddHeadersOnChange={requestAddHeadersOnChange}
requestBodyOnChange={requestBodyOnChange}
requestBodyEnabledOnChange={requestBodyEnabledOnChange}
requestSampleInputOnChange={requestSampleInputOnChange}
requestContentTypeOnChange={requestContentTypeOnChange}
requestUrlTransformOnChange={requestUrlTransformOnChange}

View File

@ -897,6 +897,17 @@ export type RequestTransformContentType =
| 'application/json'
| 'application/x-www-form-urlencoded';
export type RequestTransformBodyActions =
| 'remove'
| 'transform'
| 'x_www_form_urlencoded';
export type RequestTransformBody = {
action: RequestTransformBodyActions;
template?: string;
form_template?: Record<string, string>;
};
export type RequestTransformHeaders = {
add_headers?: Record<string, string>;
remove_headers?: string[];
@ -920,7 +931,7 @@ interface RequestTransformV1 extends RequestTransformFields {
interface RequestTransformV2 extends RequestTransformFields {
version: 2;
body?: Record<string, Nullable<string>>;
body?: RequestTransformBody;
}
export type RequestTransform = RequestTransformV1 | RequestTransformV2;

View File

@ -380,117 +380,32 @@ then you should provide the template URL as `{{$base_url}}/{{escapeUri $session_
:::
### Request Content-Type
You can change the `Content-Type` of the request to either `application/json` or `x-www-form-urlencoded`. The default is `application/json`.
<Tabs className="api-tabs">
<TabItem value="console" label="Console">
Console support coming soon.
</TabItem>
<TabItem value="cli" label="CLI">
Update the `actions.yaml` file inside the `metadata` directory and add a [request_transform][RequestTransformation] field to the action:
```yaml {9}
- name: create_user
definition:
kind: synchronous
handler: https://action.my_app.com/create-user
timeout: 60
request_transform:
template_engine: Kriti
method: POST
content_type: application/json
url: '{{$base_url}}/create_user'
query_params:
id: '{{$session_variables[''x-hasura-user-id'']}}'
body: '{"username": {{$body.input.username}}}'
comment: Custom action to create user
```
Apply the metadata by running:
```bash
hasura metadata apply
```
</TabItem>
<TabItem value="api" label="API">
REST Connectors can be configured for actions using the [create_action][metadata-create-action] or
[update_action][metadata-update-action] metadata APIs by adding a
[request_transform][RequestTransformation] field to the args:
```http {31}
POST /v1/metadata HTTP/1.1
Content-Type: application/json
X-Hasura-Role: admin
{
"type":"create_action",
"args":{
"name":"create_user",
"definition":{
"kind":"synchronous",
"arguments":[
{
"name":"username",
"type":"String!"
},
{
"name":"email",
"type":"String!"
}
],
"output_type":"User",
"handler":"{{ACTION_BASE_URL}}",
"timeout":60,
"request_transform": {
"template_engine": "Kriti",
"method": "POST",
"url": "{{$base_url}}/create_user",
"query_params": {
"id": "{{$session_variables['x-hasura-user-id']}}"
},
"content_type": "application/json",
"body": "{\"username\": {{$body.input.username}}}"
}
},
"comment": "Custom action to create user"
}
}
```
</TabItem>
</Tabs>
With `x-www-form-urlencoded`, the key-value pairs in `body` are transformed to `name={{$body.input.name}}&key2={{$body.input.email}}`.
### Request Body
You can generate a custom request body by configuring a template to transform the default payload to a custom payload.
The `body` field takes a template in the [Kriti templating language](https://github.com/hasura/kriti-lang) to evaluate the transform.
You can generate a custom request body by configuring a template to transform the default payload to a custom payload. Request body could be provided using the `body` field
as an [object](/graphql/core/api-reference/syntax-defs.mdx/#bodytransform), which additionally gives the ability to disable request body,
transform request body to `application/json`, or transform request body to `x_www_form_urlencoded` formats.
- [Disabling Request Body](#disabling-request-body)
- [Request Body with application/json format](#request-body-with-applicationjson-format)
- [Request Body with x_www_form_urlencoded format](#request-body-with-x_www_form_urlencoded-format)
##### Disabling Request Body
If you are using a `GET` request, you might want to not send any request body, and additionally not send a `content-type` header to the webhook. You can do that using the disable body feature.
<Tabs className="api-tabs">
<TabItem value="console" label="Console">
In the `Configure REST Connectors` section, click on `Add Payload Transform`:
A sample payload input auto-generated based on your schema is shown.
In the `Configure REST Connectors` section, click on `Add Request Options Transform`, and convert the `Request Method` as `GET`.
Then click on `Add Payload Transform`, disable the payload body by using the dropdown next to the `Configure Request Body` section.
<Thumbnail
src="/img/graphql/core/actions/transform-body.png"
alt="Add payload transformation"
src="/img/graphql/core/actions/transform-body-disable.png"
alt="Disable payload body"
width="1100px"
/>
The transformed sample payload should be shown as the `Transformed Request Body` given all required [sample context](#action-transforms-sample-context) is set.
Hit `Save Action` to apply your changes.
</TabItem>
@ -498,7 +413,7 @@ Hit `Save Action` to apply your changes.
Update the `actions.yaml` file inside the `metadata` directory and add a [request_transform][RequestTransformation] field to the action:
```yaml {13}
```yaml {8-9,13-16}
- name: create_user
definition:
kind: synchronous
@ -506,12 +421,15 @@ Update the `actions.yaml` file inside the `metadata` directory and add a [reques
timeout: 60
request_transform:
template_engine: Kriti
method: POST
content_type: application/json
version: 2
method: GET
url: '{{$base_url}}/create_user'
query_params:
id: '{{$session_variables[''x-hasura-user-id'']}}'
body: '{"username": {{$body.input.username}}}'
body:
action: 'remove'
request_headers:
remove_headers: ['content-type]
comment: Custom action to create user
```
@ -528,7 +446,7 @@ REST Connectors can be configured for actions using the [create_action][metadata
[update_action][metadata-update-action] metadata APIs by adding a
[request_transform][RequestTransformation] field to the args:
```http {32}
```http {26-27,32-37}
POST /v1/metadata HTTP/1.1
Content-Type: application/json
X-Hasura-Role: admin
@ -554,13 +472,18 @@ X-Hasura-Role: admin
"timeout":60,
"request_transform": {
"template_engine": "Kriti",
"method": "POST",
"version": 2,
"method": "GET",
"url": "{{$base_url}}/create_user",
"query_params": {
"id": "{{$session_variables['x-hasura-user-id']}}"
},
"content_type": "application/json",
"body": "{\"username\": {{$body.input.username}}}"
"body": {
"action": "remove"
},
"request_headers": {
"remove_headers": ["content-type"],
},
}
},
"comment": "Custom action to create user"
@ -571,6 +494,226 @@ X-Hasura-Role: admin
</TabItem>
</Tabs>
##### Request Body with application/json format
You can transform Request Body to `application/json` format using the following steps:
<Tabs className="api-tabs">
<TabItem value="console" label="Console">
In the `Configure REST Connectors` section, click on `Add Payload Transform`.
By default console sends the body as `application/json` which can be seen in the dropdown next to the `Configure Request Body` section.
<Thumbnail
src="/img/graphql/core/actions/transform-body-application-json.png"
alt="payload body application/json"
width="1100px"
/>
Hit `Save Action` to apply your changes.
</TabItem>
<TabItem value="cli" label="CLI">
Update the `actions.yaml` file inside the `metadata` directory and add a [request_transform][RequestTransformation] field to the action:
```yaml {8,13-15}
- name: create_user
definition:
kind: synchronous
handler: https://action.my_app.com/create-user
timeout: 60
request_transform:
template_engine: Kriti
version: 2
method: POST
url: '{{$base_url}}/create_user'
query_params:
id: '{{$session_variables[''x-hasura-user-id'']}}'
body:
action: 'transform'
template: '{"username": {{$body.input.username}}}'
comment: Custom action to create user
```
Apply the metadata by running:
```bash
hasura metadata apply
```
</TabItem>
<TabItem value="api" label="API">
REST Connectors can be configured for actions using the [create_action][metadata-create-action] or
[update_action][metadata-update-action] metadata APIs by adding a
[request_transform][RequestTransformation] field to the args:
```http {26,32-35}
POST /v1/metadata HTTP/1.1
Content-Type: application/json
X-Hasura-Role: admin
{
"type":"create_action",
"args":{
"name":"create_user",
"definition":{
"kind":"synchronous",
"arguments":[
{
"name":"username",
"type":"String!"
},
{
"name":"email",
"type":"String!"
}
],
"output_type":"User",
"handler":"{{ACTION_BASE_URL}}",
"timeout":60,
"request_transform": {
"template_engine": "Kriti",
"version": 2,
"method": "POST",
"url": "{{$base_url}}/create_user",
"query_params": {
"id": "{{$session_variables['x-hasura-user-id']}}"
},
"body": {
"action": "transform"
"template": "{\"username\": {{$body.input.username}}}"
},
}
},
"comment": "Custom action to create user"
}
}
```
</TabItem>
</Tabs>
##### Request Body with x_www_form_urlencoded format
While doing `x_www_form_urlencoded` transformation, please note that as all changes to the request must be made explicit when calling the API,
so you will need to remove the default `application/json` header and add a `application/x-www-form-urlencoded` header.
<Tabs className="api-tabs">
<TabItem value="console" label="Console">
In the `Configure REST Connectors` section, click on `Add Payload Transform`.
Change the dropdown next to `Configure Request Body` section to `x_www_form_urlencoded`. You can see the body transformed body in the `Transformed Request Body` section.
<Thumbnail
src="/img/graphql/core/actions/transform-body-xurl-formencoded.png"
alt="payload body x_www_form_urlencoded"
width="1100px"
/>
Hit `Save Action` to apply your changes.
</TabItem>
<TabItem value="cli" label="CLI">
Update the `actions.yaml` file inside the `metadata` directory and add a [request_transform][RequestTransformation] field to the action:
```yaml {8,13-20}
- name: create_user
definition:
kind: synchronous
handler: https://action.my_app.com/create-user
timeout: 60
request_transform:
template_engine: Kriti
version: 2
method: POST
url: '{{$base_url}}/create_user'
query_params:
id: '{{$session_variables[''x-hasura-user-id'']}}'
body:
action: 'x_www_form_urlencoded'
form_template:
username: '{{$body.input.username}}'
request_headers:
remove_headers: ['content-type']
add_headers:
'content-type': 'application/x-www-form-urlencoded'
comment: Custom action to create user
```
Apply the metadata by running:
```bash
hasura metadata apply
```
</TabItem>
<TabItem value="api" label="API">
REST Connectors can be configured for actions using the [create_action][metadata-create-action] or
[update_action][metadata-update-action] metadata APIs by adding a
[request_transform][RequestTransformation] field to the args:
```http {26,32-43}
POST /v1/metadata HTTP/1.1
Content-Type: application/json
X-Hasura-Role: admin
{
"type":"create_action",
"args":{
"name":"create_user",
"definition":{
"kind":"synchronous",
"arguments":[
{
"name":"username",
"type":"String!"
},
{
"name":"email",
"type":"String!"
}
],
"output_type":"User",
"handler":"{{ACTION_BASE_URL}}",
"timeout":60,
"request_transform": {
"template_engine": "Kriti",
"version": 2,
"method": "POST",
"url": "{{$base_url}}/create_user",
"query_params": {
"id": "{{$session_variables['x-hasura-user-id']}}"
},
"body": {
"action": "x_www_form_urlencoded",
"form_template": {
"username": "{{$body.input.username}}"
},
},
"request_headers": {
"remove_headers": ["content-type"],
"add_headers": {
"content-type": "application/x-www-form-urlencoded"
},
},
}
},
"comment": "Custom action to create user"
}
}
```
</TabItem>
</Tabs>
## Example
Let's integrate Auth0's management API to update the profile of a user:

View File

@ -1269,15 +1269,20 @@ Enum](https://spec.graphql.org/June2018/#sec-Enums)
| Key | required | Schema | Description |
|-----------------|----------|----------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------|
| version | false | "1" \| "2" | Sets the `RequestTransformation` schema version. Version `1` uses a `String` for the `body` field and Version `2` takes a [BodyTransform](#bodytransform). Defaults to version `1`. Warning: You must remove any "version 2" schemas from your metadata prior to downgrading to `v2.4.0` or earlier |
| version | false | "1" \| "2" | Sets the `RequestTransformation` schema version. Version `1` uses a `String` for the `body` field and Version `2` takes a [BodyTransform](#bodytransform). Defaults to version `1`. |
| method | false | String | Change the request method to this value. |
| url | false | String | Change the request URL to this value. |
| body | false | [BodyTransform](#bodytransform) \| String | A template script for transforming the request body. |
| content_type | false | String | Replace the Content-Type with this value. Only "application/json" and "application/x-www-form-urlencoded" are allowed. Default: "application/json" |
| content_type | false | String | Replace the Content-Type with this value. Default: "application/json" *(valid only for version 1)* |
| query_params | false | Object (String : String) | Replace the query params on the URL with this value. |
| request_headers | false | [TransformHeaders](#transformheaders) | Request Header Transformation. |
| template_engine | false | [TemplateEngine](#templateengine) | Template language to be used for this transformation. Default: "Kriti" |
:::tip Supported from
Version 2 is supported in `v2.5.0` and above. You must remove any "version 2" schemas from your metadata prior to downgrading to `v2.4.0` or earlier
:::
:::info Note
@ -1350,7 +1355,7 @@ HGE provides the following functions that can be used in the template:
| Key | required | Schema | Description |
|---------------|----------|---------------------|--------------------------------------------------------------------------------------------------------|
| action | true | remove \| transform | The action to perform on the request body. |
| action | true | remove \| transform \| x_www_form_urlencoded | The action to perform on the request body. |
| template | false | String | The transformation template to be applied to the body. This is required if the action is *transform*. |
| form_template | false | Object ([String](/graphql/core/databases/postgres/postgresql-types.mdx#string) : [String](/graphql/core/databases/postgres/postgresql-types.mdx#string)) | The key/value pairs to be used in a `x-www-url-formencoded` body. The values can be transfomation templates. |

View File

@ -86,7 +86,7 @@ You can configure REST Connectors for event triggers using the
[request_transform][RequestTransformation] field to the args:
```http {25-34}
POST /v1/metadata HTTP/1.1
POST /v1/metadata HTTP/1.1
Content-Type: application/json
X-Hasura-Role: admin
@ -98,7 +98,7 @@ X-Hasura-Role: admin
"source": "default",
"table": {
"name": "users",
"schema": "public"
"schema": "public"
},
"webhook": "https://api.somedomain.com",
"insert": {
@ -406,22 +406,41 @@ contain non-ASCII values, then you should provide the template URL as
:::
### Request Content-Type
### Request Body
You can change the `Content-Type` of the request to either `application/json` or `x-www-form-urlencoded`. The default is `application/json`.
You can generate a custom request body by configuring a template to transform the default payload to a custom payload. Request body could be provided using the `body` field
as an [object](/graphql/core/api-reference/syntax-defs.mdx/#bodytransform), which additionally gives the ability to disable request body,
transform request body to `application/json`, or transform request body to `x_www_form_urlencoded` formats.
- [Disabling Request Body](#disabling-request-body)
- [Request Body with application/json format](#request-body-with-applicationjson-format)
- [Request Body with x_www_form_urlencoded format](#request-body-with-x_www_form_urlencoded-format)
##### Disabling Request Body
If you are using a `GET` request, you might want to not send any request body, and additionally not send a `content-type` header to the webhook. You can do that using the disable body feature.
<Tabs className="api-tabs">
<TabItem value="console" label="Console">
Console support coming soon.
In the `Configure REST Connectors` section, click on `Add Request Options Transform`, and convert the `Request Method` as `GET`.
Then click on `Add Payload Transform`, disable the payload body by using the dropdown next to the `Configure Request Body` section.
<Thumbnail
src="/img/graphql/core/event-triggers/transform-body-disable.png"
alt="Disable payload body"
width="1100px"
/>
Hit `Create Event Trigger` to apply your changes.
</TabItem>
<TabItem value="cli" label="CLI">
Update the `databases > [source-name] > tables > [table-name].yaml` file
inside the `metadata` directory and add a [request_transform][RequestTransformation] field to the event trigger:
Update the `databases > [source-name] > tables > [table-name].yaml` file inside the `metadata` directory and add a
[request_transform][RequestTransformation] field to the event trigger:
```yaml {19}
```yaml {15-16,20-23}
table:
name: users
schema: public
@ -436,12 +455,15 @@ event_triggers:
value: bearer-xxxx
request_transform:
template_engine: Kriti
method: POST
version: 2
method: GET
url: "{{$base_url}}/api/v3/endpoint"
query_params:
query_param: xxxxx
content_type: application/json
body: "{\n \"table\": {\n \"name\": {{$body.table.name}},\n \"schema\": {{$body.table.schema}}\n },\n \"To\": {\n \"username\": {{$body.event.data.new.name}},\n \"email\": {{$body.event.data.new.email}}\n }\n}"
body:
action: 'remove'
request_headers:
remove_headers: ['content-type]
```
Apply the metadata by running:
@ -457,8 +479,8 @@ You can configure REST Connectors for event triggers using the
[pg_create_event_trigger][metadata-pg-create-event-trigger] metadata API and adding a
[request_transform][RequestTransformation] field to the args:
```http {32}
POST /v1/metadata HTTP/1.1
```http {27-28,33-38}
POST /v1/metadata HTTP/1.1
Content-Type: application/json
X-Hasura-Role: admin
@ -484,13 +506,18 @@ X-Hasura-Role: admin
],
"request_transform": {
"template_engine": "Kriti",
"method": "POST",
"version": 2,
"method": "GET",
"url": "{{$base_url}}/api/v3/endpoint",
"query_params": {
"query_param": "xxxxx"
},
"content_type": "application/json",
"body": "{\n \"table\": {\n \"name\": {{$body.table.name}},\n \"schema\": {{$body.table.schema}}\n },\n \"To\": {\n \"username\": {{$body.event.data.new.name}},\n \"email\": {{$body.event.data.new.email}}\n }\n}"
"body": {
"action": "remove"
},
"request_headers": {
"remove_headers": ["content-type"],
},
}
}
}
@ -499,40 +526,24 @@ X-Hasura-Role: admin
</TabItem>
</Tabs>
With `x-www-form-urlencoded`, the key-value pairs in `body` are transformed to `name={{$body.input.name}}&key2={{$body.input.email}}`.
### Request Body
##### Request Body with application/json format
You can generate a custom request body by configuring a template to transform the default payload to a custom payload.
The `body` field takes a template in the [Kriti templating language](https://github.com/hasura/kriti-lang) to evaluate the transform.
For example, you can obtain the following transformed event trigger request body for a `users` table:
<Thumbnail
src="/img/graphql/core/event-triggers/transform-request-body.png"
alt="Transformed Request Body"
width="900px"
/>
You can transform Request Body to `application/json` format using the following steps:
<Tabs className="api-tabs">
<TabItem value="console" label="Console">
Head to `Events > [event_trigger_name] > Modify` tab. Under `Configure REST Connectors` click on `Add Payload Transform`.
A sample payload input auto-generated based on your schema is shown.
Under `Configure Request Body` enter the required request body template:
In the `Configure REST Connectors` section, click on `Add Payload Transform`.
By default console sends the body as `application/json` which can be seen in the dropdown next to the `Configure Request Body` section.
<Thumbnail
src="/img/graphql/core/event-triggers/transform-generic-request-body.png"
alt="Configure request body"
src="/img/graphql/core/event-triggers/transform-body-application-json.png"
alt="payload body application/json"
width="1100px"
/>
The transformed sample payload should be shown as the `Transformed Request Body` given all required
[sample context](#event-trigger-transforms-sample-context) is set.
Hit `Save Event Trigger` to apply your changes.
Hit `Create Event Trigger` to apply your changes.
</TabItem>
<TabItem value="cli" label="CLI">
@ -540,7 +551,7 @@ Hit `Save Event Trigger` to apply your changes.
Update the `databases > [source-name] > tables > [table-name].yaml` file inside the `metadata` directory and add a
[request_transform][RequestTransformation] field to the event trigger:
```yaml {20}
```yaml {15,20-22}
table:
name: users
schema: public
@ -555,18 +566,20 @@ event_triggers:
value: bearer-xxxx
request_transform:
template_engine: Kriti
version: 2
method: POST
url: "{{$base_url}}/api/v3/endpoint"
query_params:
query_param: xxxxx
content_type: application/json
body: "{\n \"table\": {\n \"name\": {{$body.table.name}},\n \"schema\": {{$body.table.schema}}\n },\n \"To\": {\n \"username\": {{$body.event.data.new.name}},\n \"email\": {{$body.event.data.new.email}}\n }\n}"
body:
action: 'transform'
template: '{"username": {{$body.table.username}}}'
```
Apply the metadata by running:
```bash
hasura metadata apply
hasura metadata apply
```
</TabItem>
@ -576,8 +589,8 @@ You can configure REST Connectors for event triggers using the
[pg_create_event_trigger][metadata-pg-create-event-trigger] metadata API and adding a
[request_transform][RequestTransformation] field to the args:
```http {33}
POST /v1/metadata HTTP/1.1
```http {27,33-36}
POST /v1/metadata HTTP/1.1
Content-Type: application/json
X-Hasura-Role: admin
@ -603,13 +616,16 @@ X-Hasura-Role: admin
],
"request_transform": {
"template_engine": "Kriti",
"version": 2,
"method": "POST",
"url": "{{$base_url}}/api/v3/endpoint",
"query_params": {
"query_param": "xxxxx"
},
"content_type": "application/json",
"body": "{\n \"table\": {\n \"name\": {{$body.table.name}},\n \"schema\": {{$body.table.schema}}\n },\n \"To\": {\n \"username\": {{$body.event.data.new.name}},\n \"email\": {{$body.event.data.new.email}}\n }\n}"
"body": {
"action": "transform",
"template": "{\"username\": {{$body.table.username}}}"
},
}
}
}
@ -618,6 +634,129 @@ X-Hasura-Role: admin
</TabItem>
</Tabs>
##### Request Body with x_www_form_urlencoded format
While doing `x_www_form_urlencoded` transformation, please note that as all changes to the request must be made explicit when calling the API,
so you will need to remove the default `application/json` header and add a `application/x-www-form-urlencoded` header.
<Tabs className="api-tabs">
<TabItem value="console" label="Console">
In the `Configure REST Connectors` section, click on `Add Payload Transform`.
Change the dropdown next to `Configure Request Body` section to `x_www_form_urlencoded`. You can see the body transformed body in the `Transformed Request Body` section.
<Thumbnail
src="/img/graphql/core/event-triggers/transform-body-xurl-formencoded.png"
alt="payload body x_www_form_urlencoded"
width="1100px"
/>
Hit `Create Event Trigger` to apply your changes.
</TabItem>
<TabItem value="cli" label="CLI">
Update the `databases > [source-name] > tables > [table-name].yaml` file inside the `metadata` directory and add a
[request_transform][RequestTransformation] field to the event trigger:
```yaml {15,20-27}
table:
name: users
schema: public
event_triggers:
- name: insert_trigger_on_users
definition:
insert:
columns: "*"
webhook: https://api.somedomain.com
headers:
- name: Authorization
value: bearer-xxxx
request_transform:
template_engine: Kriti
version: 2
method: POST
url: "{{$base_url}}/api/v3/endpoint"
query_params:
query_param: xxxxx
body:
action: 'x_www_form_urlencoded'
form_template:
username: '{{$body.table.username}}'
request_headers:
remove_headers: ['content-type']
add_headers:
'content-type': 'application/x-www-form-urlencoded'
```
Apply the metadata by running:
```bash
hasura metadata apply
```
</TabItem>
<TabItem value="api" label="API">
You can configure REST Connectors for event triggers using the
[pg_create_event_trigger][metadata-pg-create-event-trigger] metadata API and adding a
[request_transform][RequestTransformation] field to the args:
```http {27,33-44}
POST /v1/metadata HTTP/1.1
Content-Type: application/json
X-Hasura-Role: admin
{
"type": "pg_create_event_trigger",
"args": {
"name": "insert_trigger_on_users",
"replace": true,
"source": "default",
"table": {
"name": "users",
"schema": "public"
},
"webhook": "https://api.somedomain.com",
"insert": {
"columns": "*"
},
"headers": [
{
"name": "Authorization",
"value": "bearer xxxx"
}
],
"request_transform": {
"template_engine": "Kriti",
"version": 2,
"method": "POST",
"url": "{{$base_url}}/api/v3/endpoint",
"query_params": {
"query_param": "xxxxx"
},
"body": {
"action": "x_www_form_urlencoded",
"form_template": {
"username": "{{$body.table.username}}"
},
},
"request_headers": {
"remove_headers": ["content-type"],
"add_headers": {
"content-type": "application/x-www-form-urlencoded"
},
},
}
}
}
```
</TabItem>
</Tabs>
## Example: Trigger SendGrid's Mail Send API
To see the REST Connectors for event triggers in action, let's set up an event trigger to send an email using the [SendGrid Mail Send API](https://docs.sendgrid.com/api-reference/mail-send/mail-send).

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB