Skip to main content
Glama
index.ts12.4 kB
import { OAuth2PropertyValue, PieceAuthProperty, Property, StaticDropdownProperty, createAction, StaticPropsValue, InputPropertyMap, FilesService, DynamicPropsValue, AppConnectionValueForAuthProperty, ExtractPieceAuthPropertyTypeForMethods, ApFile, } from '@activepieces/pieces-framework'; import { HttpError, HttpHeaders, HttpMethod, HttpRequest, QueryParams, httpClient, } from '../http'; import { assertNotNullOrUndefined, isEmpty, isNil } from '@activepieces/shared'; import fs from 'fs'; import mime from 'mime-types'; import FormData from 'form-data'; export const getAccessTokenOrThrow = ( auth: OAuth2PropertyValue | undefined ): string => { const accessToken = auth?.access_token; if (accessToken === undefined) { throw new Error('Invalid bearer token'); } return accessToken; }; const joinBaseUrlWithRelativePath = ({ baseUrl, relativePath, }: { baseUrl: string; relativePath: string; }) => { const baseUrlWithSlash = baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`; const relativePathWithoutSlash = relativePath.startsWith('/') ? relativePath.slice(1) : relativePath; return `${baseUrlWithSlash}${relativePathWithoutSlash}`; }; const getBaseUrlForDescription = < PieceAuth extends PieceAuthProperty | PieceAuthProperty[] | undefined >( baseUrl: BaseUrlGetter<PieceAuth>, auth?: AppConnectionValueForAuthProperty< ExtractPieceAuthPropertyTypeForMethods<PieceAuth> > ) => { const exampleBaseUrl = `https://api.example.com`; try { const baseUrlValue = auth ? baseUrl(auth) : undefined; const baseUrlValueWithoutTrailingSlash = baseUrlValue?.endsWith('/') ? baseUrlValue.slice(0, -1) : baseUrlValue; return baseUrlValueWithoutTrailingSlash ?? exampleBaseUrl; } catch (error) { //If baseUrl fails we stil want to return a valid baseUrl for description { return exampleBaseUrl; } } }; type BaseUrlGetter< PieceAuth extends PieceAuthProperty | PieceAuthProperty[] | undefined > = ( auth?: AppConnectionValueForAuthProperty< ExtractPieceAuthPropertyTypeForMethods<PieceAuth> > ) => string; export function createCustomApiCallAction< PieceAuth extends PieceAuthProperty | PieceAuthProperty[] | undefined >({ auth, baseUrl, authMapping, description, displayName, name, props, extraProps, authLocation = 'headers', }: { auth?: PieceAuth; baseUrl: BaseUrlGetter<PieceAuth>; authMapping?: ( auth: AppConnectionValueForAuthProperty< ExtractPieceAuthPropertyTypeForMethods<PieceAuth> >, propsValue: StaticPropsValue<any> ) => Promise<HttpHeaders | QueryParams>; // add description as a parameter that can be null description?: string | null; displayName?: string | null; name?: string | null; props?: { url?: Partial<ReturnType<typeof Property.ShortText>>; method?: Partial<StaticDropdownProperty<HttpMethod, boolean>>; headers?: Partial<ReturnType<typeof Property.Object>>; queryParams?: Partial<ReturnType<typeof Property.Object>>; body?: Partial<ReturnType<typeof Property.Json>>; failsafe?: Partial<ReturnType<typeof Property.Checkbox>>; timeout?: Partial<ReturnType<typeof Property.Number>>; }; extraProps?: InputPropertyMap; authLocation?: 'headers' | 'queryParams'; }) { return createAction({ name: name ? name : 'custom_api_call', displayName: displayName ? displayName : 'Custom API Call', description: description ? description : 'Make a custom API call to a specific endpoint', auth, requireAuth: auth ? true : false, props: { url: Property.DynamicProperties({ auth, displayName: '', required: true, refreshers: [], props: async ({ auth }) => { return { url: Property.ShortText({ displayName: 'URL', description: `You can either use the full URL or the relative path to the base URL i.e ${getBaseUrlForDescription(baseUrl, auth)}/resource or /resource`, required: true, defaultValue: auth ? baseUrl(auth) : '', ...(props?.url ?? {}), }), }; }, }), method: Property.StaticDropdown({ displayName: 'Method', required: true, options: { options: Object.values(HttpMethod).map((v) => { return { label: v, value: v, }; }), }, ...(props?.method ?? {}), }), headers: Property.Object({ displayName: 'Headers', description: 'Authorization headers are injected automatically from your connection.', required: true, ...(props?.headers ?? {}), }), queryParams: Property.Object({ displayName: 'Query Parameters', required: true, ...(props?.queryParams ?? {}), }), body_type: Property.StaticDropdown({ displayName: 'Body Type', required: false, defaultValue: 'none', options: { disabled: false, options: [ { label: 'None', value: 'none', }, { label: 'JSON', value: 'json', }, { label: 'Form Data', value: 'form_data', }, { label: 'Raw', value: 'raw', }, ], }, }), body: Property.DynamicProperties({ auth, displayName: 'Body', refreshers: ['body_type'], required: false, props: async ({ body_type }) => { if (!body_type) return {}; const bodyTypeInput = body_type as unknown as string; const fields: DynamicPropsValue = {}; switch (bodyTypeInput) { case 'none': break; case 'json': fields['data'] = Property.Json({ displayName: 'JSON Body', required: true, ...(props?.body ?? {}), }); break; case 'raw': fields['data'] = Property.LongText({ displayName: 'Raw Body', required: true, }); break; case 'form_data': fields['data'] = Property.Array({ displayName: 'Form Data', required: true, properties: { fieldName: Property.ShortText({ displayName: 'Field Name', required: true, }), fieldType: Property.StaticDropdown({ displayName: 'Field Type', required: true, options: { disabled: false, options: [ { label: 'Text', value: 'text' }, { label: 'File', value: 'file' }, ], }, }), textFieldValue: Property.LongText({ displayName: 'Text Field Value', required: false, }), fileFieldValue: Property.File({ displayName: 'File Field Value', required: false, }), }, }); break; } return fields; }, }), response_is_binary: Property.Checkbox({ displayName: 'Response is Binary ?', description: 'Enable for files like PDFs, images, etc.', required: false, defaultValue: false, }), failsafe: Property.Checkbox({ displayName: 'No Error on Failure', required: false, ...(props?.failsafe ?? {}), }), timeout: Property.Number({ displayName: 'Timeout (in seconds)', required: false, ...(props?.timeout ?? {}), }), ...extraProps, }, run: async (context) => { const { method, url, headers, queryParams, body, body_type, failsafe, timeout, response_is_binary, } = context.propsValue; assertNotNullOrUndefined(method, 'Method'); assertNotNullOrUndefined(url, 'URL'); const authValue = !isNil(authMapping) ? await authMapping(context.auth, context.propsValue) : {}; const urlValue = url['url'] as string; const fullUrl = urlValue.startsWith('http://') || urlValue.startsWith('https://') ? urlValue : joinBaseUrlWithRelativePath({ baseUrl: baseUrl(context.auth), relativePath: urlValue, }); const request: HttpRequest = { method, url: fullUrl, headers: { ...((headers ?? {}) as HttpHeaders), ...(authLocation === 'headers' || !isNil(authLocation) ? authValue : {}), }, queryParams: { ...(authLocation === 'queryParams' ? (authValue as QueryParams) : {}), ...((queryParams as QueryParams) ?? {}), }, timeout: timeout ? timeout * 1000 : 0, }; // Set response type to arraybuffer if binary response is expected if (response_is_binary) { request.responseType = 'arraybuffer'; } if (body) { if (body_type && body_type !== 'none') { const bodyInput = body['data']; if (body_type === 'form_data') { const formBodyInput = bodyInput as Array<{ fieldName: string; fieldType: 'text' | 'file'; textFieldValue?: string; fileFieldValue?: ApFile; }>; const formData = new FormData(); for (const { fieldName, fieldType, textFieldValue, fileFieldValue, } of formBodyInput) { if (fieldType === 'text' && !isEmpty(textFieldValue)) { formData.append(fieldName, textFieldValue); } else if (fieldType === 'file' && !isEmpty(fileFieldValue)) { formData.append(fieldName, fileFieldValue!.data, { filename: fileFieldValue?.filename, }); } } request.body = formData; request.headers = { ...request.headers, ...formData.getHeaders() }; } else { request.body = bodyInput; } } else if (!body_type) { request.body = body; } } try { const response = await httpClient.sendRequest(request); return await handleBinaryResponse( context.files, response.body, response.status, response.headers, response_is_binary ); } catch (error) { if (failsafe) { return (error as HttpError).errorMessage(); } throw error; } }, }); } export function is_chromium_installed(): boolean { const chromiumPath = '/usr/bin/chromium'; return fs.existsSync(chromiumPath); } const handleBinaryResponse = async ( files: FilesService, bodyContent: string | ArrayBuffer | Buffer, status: number, headers?: HttpHeaders, isBinary?: boolean ) => { let body; if (isBinary && isBinaryBody(bodyContent)) { const contentTypeValue = Array.isArray(headers?.['content-type']) ? headers['content-type'][0] : headers?.['content-type']; const fileExtension: string = mime.extension(contentTypeValue ?? '') || 'txt'; let bufferData: Buffer; if (bodyContent instanceof ArrayBuffer) { bufferData = Buffer.from(new Uint8Array(bodyContent)); } else if (Buffer.isBuffer(bodyContent)) { bufferData = bodyContent; } else { bufferData = Buffer.from(bodyContent); } body = await files.write({ fileName: `output.${fileExtension}`, data: bufferData, }); } else { body = bodyContent; } return { status, headers, body }; }; const isBinaryBody = (body: string | ArrayBuffer | Buffer) => { return body instanceof ArrayBuffer || Buffer.isBuffer(body); };

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/activepieces/activepieces'

If you have feedback or need assistance with the MCP directory API, please join our Discord server