Add Activepieces integration for workflow automation
- Add Activepieces fork with SmoothSchedule custom piece - Create integrations app with Activepieces service layer - Add embed token endpoint for iframe integration - Create Automations page with embedded workflow builder - Add sidebar visibility fix for embed mode - Add list inactive customers endpoint to Public API - Include SmoothSchedule triggers: event created/updated/cancelled - Include SmoothSchedule actions: create/update/cancel events, list resources/services/customers 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
export * from './lib/authentication';
|
||||
export * from './lib/helpers';
|
||||
export * from './lib/http';
|
||||
export * from './lib/polling';
|
||||
export * from './lib/validation';
|
||||
@@ -0,0 +1,21 @@
|
||||
export type Authentication = BearerTokenAuthentication | BasicAuthentication;
|
||||
|
||||
export enum AuthenticationType {
|
||||
BEARER_TOKEN = 'BEARER_TOKEN',
|
||||
BASIC = 'BASIC',
|
||||
}
|
||||
|
||||
export type BaseAuthentication<T extends AuthenticationType> = {
|
||||
type: T;
|
||||
};
|
||||
|
||||
export type BearerTokenAuthentication =
|
||||
BaseAuthentication<AuthenticationType.BEARER_TOKEN> & {
|
||||
token: string;
|
||||
};
|
||||
|
||||
export type BasicAuthentication =
|
||||
BaseAuthentication<AuthenticationType.BASIC> & {
|
||||
username: string;
|
||||
password: string;
|
||||
};
|
||||
@@ -0,0 +1,283 @@
|
||||
import {
|
||||
OAuth2PropertyValue,
|
||||
PieceAuthProperty,
|
||||
Property,
|
||||
StaticDropdownProperty,
|
||||
createAction,
|
||||
StaticPropsValue,
|
||||
InputPropertyMap,
|
||||
FilesService,
|
||||
AppConnectionValueForAuthProperty,
|
||||
ExtractPieceAuthPropertyTypeForMethods,
|
||||
} from '@activepieces/pieces-framework';
|
||||
import {
|
||||
HttpError,
|
||||
HttpHeaders,
|
||||
HttpMethod,
|
||||
HttpRequest,
|
||||
QueryParams,
|
||||
httpClient,
|
||||
} from '../http';
|
||||
import { assertNotNullOrUndefined, isNil } from '@activepieces/shared';
|
||||
import fs from 'fs';
|
||||
import mime from 'mime-types';
|
||||
|
||||
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: Property.Json({
|
||||
displayName: 'Body',
|
||||
required: false,
|
||||
...(props?.body ?? {}),
|
||||
}),
|
||||
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,
|
||||
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<Record<string, unknown>> = {
|
||||
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) {
|
||||
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';
|
||||
body = await files.write({
|
||||
fileName: `output.${fileExtension}`,
|
||||
data: Buffer.from(bodyContent as any ),
|
||||
});
|
||||
} else {
|
||||
body = bodyContent;
|
||||
}
|
||||
|
||||
return { status, headers, body };
|
||||
};
|
||||
|
||||
const isBinaryBody = (body: string | ArrayBuffer | Buffer) => {
|
||||
return body instanceof ArrayBuffer || Buffer.isBuffer(body);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import axios, { AxiosInstance, AxiosRequestConfig, AxiosError } from 'axios';
|
||||
import axiosRetry from 'axios-retry';
|
||||
import { DelegatingAuthenticationConverter } from '../core/delegating-authentication-converter';
|
||||
import { BaseHttpClient } from '../core/base-http-client';
|
||||
import { HttpError } from '../core/http-error';
|
||||
import { HttpHeaders } from '../core/http-headers';
|
||||
import { HttpMessageBody } from '../core/http-message-body';
|
||||
import { HttpMethod } from '../core/http-method';
|
||||
import { HttpRequest } from '../core/http-request';
|
||||
import { HttpResponse } from '../core/http-response';
|
||||
import { HttpRequestBody } from '../core/http-request-body';
|
||||
|
||||
export class AxiosHttpClient extends BaseHttpClient {
|
||||
constructor(
|
||||
baseUrl = '',
|
||||
authenticationConverter: DelegatingAuthenticationConverter = new DelegatingAuthenticationConverter()
|
||||
) {
|
||||
super(baseUrl, authenticationConverter);
|
||||
}
|
||||
|
||||
async sendRequest<ResponseBody extends HttpMessageBody = any>(
|
||||
request: HttpRequest<HttpRequestBody>,
|
||||
axiosClient?: AxiosInstance
|
||||
): Promise<HttpResponse<ResponseBody>> {
|
||||
try {
|
||||
const axiosInstance = axiosClient || axios;
|
||||
process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = '0';
|
||||
const { urlWithoutQueryParams, queryParams: urlQueryParams } = this.getUrl(request);
|
||||
const headers = this.getHeaders(request);
|
||||
const axiosRequestMethod = this.getAxiosRequestMethod(request.method);
|
||||
const timeout = request.timeout ? request.timeout : 0;
|
||||
const queryParams = request.queryParams || {}
|
||||
const responseType = request.responseType || 'json';
|
||||
|
||||
for (const [key, value] of Object.entries(queryParams)) {
|
||||
urlQueryParams.append(key, value);
|
||||
}
|
||||
|
||||
const config: AxiosRequestConfig = {
|
||||
method: axiosRequestMethod,
|
||||
url: urlWithoutQueryParams,
|
||||
params: urlQueryParams,
|
||||
headers,
|
||||
data: request.body,
|
||||
timeout,
|
||||
responseType,
|
||||
};
|
||||
|
||||
if (request.retries && request.retries > 0) {
|
||||
axiosRetry(axiosInstance, {
|
||||
retries: request.retries,
|
||||
retryDelay: axiosRetry.exponentialDelay,
|
||||
retryCondition: (error) => {
|
||||
return axiosRetry.isNetworkOrIdempotentRequestError(error) || (error.response && error.response.status >= 500) || false;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const response = await axiosInstance.request(config);
|
||||
|
||||
return {
|
||||
status: response.status,
|
||||
headers: response.headers as HttpHeaders,
|
||||
body: response.data,
|
||||
};
|
||||
} catch (e) {
|
||||
if (axios.isAxiosError(e)) {
|
||||
const httpError = new HttpError(request.body, e);
|
||||
console.error(
|
||||
'[HttpClient#(sanitized error message)] Request failed:',
|
||||
httpError
|
||||
);
|
||||
throw httpError;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private getAxiosRequestMethod(httpMethod: HttpMethod): string {
|
||||
return httpMethod.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import { Authentication } from '../../authentication';
|
||||
import { DelegatingAuthenticationConverter } from './delegating-authentication-converter';
|
||||
import type { HttpClient } from './http-client';
|
||||
import { HttpHeader } from './http-header';
|
||||
import type { HttpHeaders } from './http-headers';
|
||||
import type { HttpMessageBody } from './http-message-body';
|
||||
import type { HttpRequest } from './http-request';
|
||||
import { HttpRequestBody } from './http-request-body';
|
||||
import { HttpResponse } from './http-response';
|
||||
import { MediaType } from './media-type';
|
||||
|
||||
export abstract class BaseHttpClient implements HttpClient {
|
||||
constructor(
|
||||
private readonly baseUrl: string,
|
||||
private readonly authenticationConverter: DelegatingAuthenticationConverter
|
||||
) {}
|
||||
|
||||
abstract sendRequest<
|
||||
RequestBody extends HttpMessageBody,
|
||||
ResponseBody extends HttpMessageBody
|
||||
>(request: HttpRequest<RequestBody>): Promise<HttpResponse<ResponseBody>>;
|
||||
|
||||
protected getUrl<RequestBody extends HttpMessageBody>(
|
||||
request: HttpRequest<RequestBody>
|
||||
): {
|
||||
urlWithoutQueryParams: string;
|
||||
queryParams: URLSearchParams;
|
||||
} {
|
||||
const url = new URL(`${this.baseUrl}${request.url}`);
|
||||
const urlWithoutQueryParams = `${url.origin}${url.pathname}`;
|
||||
const queryParams = new URLSearchParams();
|
||||
// Extract query parameters
|
||||
url.searchParams.forEach((value, key) => {
|
||||
queryParams.append(key, value);
|
||||
});
|
||||
return {
|
||||
urlWithoutQueryParams,
|
||||
queryParams,
|
||||
};
|
||||
}
|
||||
|
||||
protected getHeaders<RequestBody extends HttpRequestBody>(
|
||||
request: HttpRequest<RequestBody>
|
||||
): HttpHeaders {
|
||||
let requestHeaders: HttpHeaders = {
|
||||
[HttpHeader.ACCEPT]: MediaType.APPLICATION_JSON,
|
||||
};
|
||||
|
||||
if (request.authentication) {
|
||||
this.populateAuthentication(request.authentication, requestHeaders);
|
||||
}
|
||||
|
||||
if (request.body) {
|
||||
switch (request.headers?.['Content-Type']) {
|
||||
case 'text/csv':
|
||||
requestHeaders[HttpHeader.CONTENT_TYPE] = MediaType.TEXT_CSV;
|
||||
break;
|
||||
|
||||
default:
|
||||
requestHeaders[HttpHeader.CONTENT_TYPE] = MediaType.APPLICATION_JSON;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (request.headers) {
|
||||
requestHeaders = { ...requestHeaders, ...request.headers };
|
||||
}
|
||||
return requestHeaders;
|
||||
}
|
||||
|
||||
private populateAuthentication(
|
||||
authentication: Authentication,
|
||||
headers: HttpHeaders
|
||||
): void {
|
||||
this.authenticationConverter.convert(authentication, headers);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import type { HttpHeaders } from './http-headers';
|
||||
import { HttpHeader } from './http-header';
|
||||
import {
|
||||
Authentication,
|
||||
AuthenticationType,
|
||||
BasicAuthentication,
|
||||
BearerTokenAuthentication,
|
||||
} from '../../authentication';
|
||||
|
||||
export class DelegatingAuthenticationConverter
|
||||
implements AuthenticationConverter<Authentication>
|
||||
{
|
||||
private readonly converters: Record<
|
||||
AuthenticationType,
|
||||
AuthenticationConverter<any>
|
||||
>;
|
||||
|
||||
constructor(
|
||||
bearerTokenConverter = new BearerTokenAuthenticationConverter(),
|
||||
basicTokenConverter = new BasicTokenAuthenticationConverter()
|
||||
) {
|
||||
this.converters = {
|
||||
[AuthenticationType.BEARER_TOKEN]: bearerTokenConverter,
|
||||
[AuthenticationType.BASIC]: basicTokenConverter,
|
||||
};
|
||||
}
|
||||
|
||||
convert(authentication: Authentication, headers: HttpHeaders): HttpHeaders {
|
||||
const converter = this.converters[authentication.type];
|
||||
return converter.convert(authentication, headers);
|
||||
}
|
||||
}
|
||||
|
||||
class BearerTokenAuthenticationConverter
|
||||
implements AuthenticationConverter<BearerTokenAuthentication>
|
||||
{
|
||||
convert(
|
||||
authentication: BearerTokenAuthentication,
|
||||
headers: HttpHeaders
|
||||
): HttpHeaders {
|
||||
headers[HttpHeader.AUTHORIZATION] = `Bearer ${authentication.token}`;
|
||||
return headers;
|
||||
}
|
||||
}
|
||||
|
||||
class BasicTokenAuthenticationConverter
|
||||
implements AuthenticationConverter<BasicAuthentication>
|
||||
{
|
||||
convert(
|
||||
authentication: BasicAuthentication,
|
||||
headers: HttpHeaders
|
||||
): HttpHeaders {
|
||||
const credentials = `${authentication.username}:${authentication.password}`;
|
||||
const encoded = Buffer.from(credentials).toString('base64');
|
||||
headers[HttpHeader.AUTHORIZATION] = `Basic ${encoded}`;
|
||||
return headers;
|
||||
}
|
||||
}
|
||||
|
||||
type AuthenticationConverter<T extends Authentication> = {
|
||||
convert: (authentication: T, headers: HttpHeaders) => HttpHeaders;
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
import { AxiosHttpClient } from '../axios/axios-http-client';
|
||||
import type { HttpMessageBody } from './http-message-body';
|
||||
import type { HttpRequest } from './http-request';
|
||||
import { HttpRequestBody } from './http-request-body';
|
||||
import { HttpResponse } from './http-response';
|
||||
|
||||
export type HttpClient = {
|
||||
sendRequest<
|
||||
RequestBody extends HttpRequestBody,
|
||||
ResponseBody extends HttpMessageBody
|
||||
>(
|
||||
request: HttpRequest<RequestBody>
|
||||
): Promise<HttpResponse<ResponseBody>>;
|
||||
};
|
||||
|
||||
export const httpClient = new AxiosHttpClient();
|
||||
@@ -0,0 +1,51 @@
|
||||
import { AxiosError } from 'axios';
|
||||
|
||||
export class HttpError extends Error {
|
||||
private readonly status: number;
|
||||
private readonly responseBody: unknown;
|
||||
|
||||
constructor(private readonly requestBody: unknown, err: AxiosError) {
|
||||
const status = err?.response?.status || 500;
|
||||
const responseBody = err?.response?.data;
|
||||
|
||||
super(
|
||||
JSON.stringify({
|
||||
response: {
|
||||
status: status,
|
||||
body: responseBody,
|
||||
},
|
||||
request: {
|
||||
body: requestBody,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
this.status = status;
|
||||
this.responseBody = responseBody;
|
||||
}
|
||||
|
||||
public errorMessage() {
|
||||
return {
|
||||
response: {
|
||||
status: this.status,
|
||||
body: this.responseBody,
|
||||
},
|
||||
request: {
|
||||
body: this.requestBody,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
get response() {
|
||||
return {
|
||||
status: this.status,
|
||||
body: this.responseBody,
|
||||
};
|
||||
}
|
||||
|
||||
get request() {
|
||||
return {
|
||||
body: this.requestBody,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export enum HttpHeader {
|
||||
AUTHORIZATION = 'Authorization',
|
||||
ACCEPT = 'Accept',
|
||||
API_KEY = 'x-api-key',
|
||||
CONTENT_TYPE = 'Content-Type',
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export type HttpHeaders = Record<string, string | string[] | undefined>;
|
||||
@@ -0,0 +1 @@
|
||||
export type HttpMessageBody = any
|
||||
@@ -0,0 +1,8 @@
|
||||
export enum HttpMethod {
|
||||
GET = 'GET',
|
||||
POST = 'POST',
|
||||
PATCH = 'PATCH',
|
||||
PUT = 'PUT',
|
||||
DELETE = 'DELETE',
|
||||
HEAD = 'HEAD',
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import { HttpMessageBody } from "./http-message-body";
|
||||
|
||||
export type HttpRequestBody = HttpMessageBody | string;
|
||||
@@ -0,0 +1,17 @@
|
||||
import type { HttpMethod } from './http-method';
|
||||
import type { QueryParams } from './query-params';
|
||||
import { HttpHeaders } from './http-headers';
|
||||
import { Authentication } from '../../authentication';
|
||||
import { HttpRequestBody } from './http-request-body';
|
||||
|
||||
export type HttpRequest<RequestBody extends HttpRequestBody = any> = {
|
||||
method: HttpMethod;
|
||||
url: string;
|
||||
body?: RequestBody | undefined;
|
||||
headers?: HttpHeaders;
|
||||
authentication?: Authentication | undefined;
|
||||
queryParams?: QueryParams | undefined;
|
||||
timeout?: number;
|
||||
retries?: number;
|
||||
responseType?: 'arraybuffer' | 'json' | 'blob' | 'text';
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
import { HttpMessageBody } from './http-message-body';
|
||||
import { HttpHeaders } from './http-headers';
|
||||
|
||||
export type HttpResponse<RequestBody extends HttpMessageBody = any> = {
|
||||
status: number;
|
||||
headers?: HttpHeaders | undefined;
|
||||
body: RequestBody;
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
export enum MediaType {
|
||||
APPLICATION_JSON = 'application/json',
|
||||
TEXT_CSV = 'text/csv',
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export type QueryParams = Record<string, string>;
|
||||
@@ -0,0 +1,13 @@
|
||||
export * from './axios/axios-http-client';
|
||||
export * from './core/base-http-client';
|
||||
export * from './core/delegating-authentication-converter';
|
||||
export * from './core/http-client';
|
||||
export * from './core/http-error';
|
||||
export * from './core/http-header';
|
||||
export * from './core/http-headers';
|
||||
export * from './core/http-message-body';
|
||||
export * from './core/http-method';
|
||||
export * from './core/http-request';
|
||||
export * from './core/http-response';
|
||||
export * from './core/media-type';
|
||||
export * from './core/query-params';
|
||||
@@ -0,0 +1,194 @@
|
||||
import { AppConnectionValueForAuthProperty, FilesService, Store } from '@activepieces/pieces-framework';
|
||||
import { isNil } from '@activepieces/shared';
|
||||
|
||||
|
||||
interface TimebasedPolling<AuthValue, PropsValue> {
|
||||
strategy: DedupeStrategy.TIMEBASED;
|
||||
items: (params: {
|
||||
auth: AuthValue;
|
||||
store: Store;
|
||||
propsValue: PropsValue;
|
||||
lastFetchEpochMS: number;
|
||||
}) => Promise<
|
||||
{
|
||||
epochMilliSeconds: number;
|
||||
data: unknown;
|
||||
}[]
|
||||
>;
|
||||
}
|
||||
|
||||
interface LastItemPolling<AuthValue extends AppConnectionValueForAuthProperty<any>, PropsValue> {
|
||||
strategy: DedupeStrategy.LAST_ITEM;
|
||||
items: (params: {
|
||||
auth: AuthValue;
|
||||
store: Store;
|
||||
files?: FilesService;
|
||||
propsValue: PropsValue;
|
||||
lastItemId: unknown;
|
||||
}) => Promise<
|
||||
{
|
||||
id: unknown;
|
||||
data: unknown;
|
||||
}[]
|
||||
>;
|
||||
}
|
||||
|
||||
export enum DedupeStrategy {
|
||||
TIMEBASED,
|
||||
LAST_ITEM,
|
||||
}
|
||||
|
||||
export type Polling<AuthValue extends AppConnectionValueForAuthProperty<any>, PropsValue> =
|
||||
| TimebasedPolling<AuthValue, PropsValue>
|
||||
| LastItemPolling<AuthValue, PropsValue>;
|
||||
|
||||
export const pollingHelper = {
|
||||
async poll<AuthValue extends AppConnectionValueForAuthProperty<any>, PropsValue>(
|
||||
polling: Polling<AuthValue, PropsValue>,
|
||||
{
|
||||
store,
|
||||
auth,
|
||||
propsValue,
|
||||
maxItemsToPoll,
|
||||
files,
|
||||
}: {
|
||||
store: Store;
|
||||
auth: AuthValue;
|
||||
propsValue: PropsValue;
|
||||
files: FilesService;
|
||||
maxItemsToPoll?: number;
|
||||
}
|
||||
): Promise<unknown[]> {
|
||||
switch (polling.strategy) {
|
||||
case DedupeStrategy.TIMEBASED: {
|
||||
const lastEpochMilliSeconds = (await store.get<number>('lastPoll'));
|
||||
if (isNil(lastEpochMilliSeconds)) {
|
||||
throw new Error("lastPoll doesn't exist in the store.");
|
||||
}
|
||||
const items = await polling.items({
|
||||
store,
|
||||
auth,
|
||||
propsValue,
|
||||
lastFetchEpochMS: lastEpochMilliSeconds,
|
||||
});
|
||||
const newLastEpochMilliSeconds = items.reduce(
|
||||
(acc, item) => Math.max(acc, item.epochMilliSeconds),
|
||||
lastEpochMilliSeconds
|
||||
);
|
||||
await store.put('lastPoll', newLastEpochMilliSeconds);
|
||||
return items
|
||||
.filter((f) => f.epochMilliSeconds > lastEpochMilliSeconds)
|
||||
.map((item) => item.data);
|
||||
}
|
||||
case DedupeStrategy.LAST_ITEM: {
|
||||
const lastItemId = await store.get<unknown>('lastItem');
|
||||
const items = await polling.items({
|
||||
store,
|
||||
auth,
|
||||
propsValue,
|
||||
lastItemId,
|
||||
files,
|
||||
});
|
||||
|
||||
const lastItemIndex = items.findIndex((f) => f.id === lastItemId);
|
||||
let newItems = [];
|
||||
if (isNil(lastItemId) || lastItemIndex == -1) {
|
||||
newItems = items ?? [];
|
||||
} else {
|
||||
newItems = items?.slice(0, lastItemIndex) ?? [];
|
||||
}
|
||||
// Sorted from newest to oldest
|
||||
if (!isNil(maxItemsToPoll)) {
|
||||
// Get the last polling.maxItemsToPoll items
|
||||
newItems = newItems.slice(-maxItemsToPoll);
|
||||
}
|
||||
const newLastItem = newItems?.[0]?.id;
|
||||
if (!isNil(newLastItem)) {
|
||||
await store.put('lastItem', newLastItem);
|
||||
}
|
||||
return newItems.map((item) => item.data);
|
||||
}
|
||||
}
|
||||
},
|
||||
async onEnable<AuthValue extends AppConnectionValueForAuthProperty<any>, PropsValue>(
|
||||
polling: Polling<AuthValue, PropsValue>,
|
||||
{
|
||||
store,
|
||||
auth,
|
||||
propsValue,
|
||||
}: { store: Store; auth: AuthValue; propsValue: PropsValue }
|
||||
): Promise<void> {
|
||||
switch (polling.strategy) {
|
||||
case DedupeStrategy.TIMEBASED: {
|
||||
await store.put('lastPoll', Date.now());
|
||||
break;
|
||||
}
|
||||
case DedupeStrategy.LAST_ITEM: {
|
||||
const items = await polling.items({
|
||||
store,
|
||||
auth,
|
||||
propsValue,
|
||||
lastItemId: null,
|
||||
});
|
||||
const lastItemId = items?.[0]?.id;
|
||||
if (!isNil(lastItemId)) {
|
||||
await store.put('lastItem', lastItemId);
|
||||
} else {
|
||||
await store.delete('lastItem');
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
async onDisable<AuthValue extends AppConnectionValueForAuthProperty<any>, PropsValue>(
|
||||
polling: Polling<AuthValue, PropsValue>,
|
||||
params: { store: Store; auth: AuthValue; propsValue: PropsValue }
|
||||
): Promise<void> {
|
||||
switch (polling.strategy) {
|
||||
case DedupeStrategy.TIMEBASED:
|
||||
case DedupeStrategy.LAST_ITEM:
|
||||
return;
|
||||
}
|
||||
},
|
||||
async test<AuthValue extends AppConnectionValueForAuthProperty<any>, PropsValue>(
|
||||
polling: Polling<AuthValue, PropsValue>,
|
||||
{
|
||||
auth,
|
||||
propsValue,
|
||||
store,
|
||||
files,
|
||||
}: { store: Store; auth: AuthValue; propsValue: PropsValue, files: FilesService }
|
||||
): Promise<unknown[]> {
|
||||
let items = [];
|
||||
switch (polling.strategy) {
|
||||
case DedupeStrategy.TIMEBASED: {
|
||||
items = await polling.items({
|
||||
store,
|
||||
auth,
|
||||
propsValue,
|
||||
lastFetchEpochMS: 0,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case DedupeStrategy.LAST_ITEM: {
|
||||
items = await polling.items({
|
||||
store,
|
||||
auth,
|
||||
propsValue,
|
||||
lastItemId: null,
|
||||
files,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
return getFirstFiveOrAll(items.map((item) => item.data));
|
||||
},
|
||||
};
|
||||
|
||||
function getFirstFiveOrAll(array: unknown[]) {
|
||||
if (array.length <= 5) {
|
||||
return array;
|
||||
} else {
|
||||
return array.slice(0, 5);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { z } from "zod"
|
||||
|
||||
export const propsValidation = {
|
||||
async validateZod<T extends Record<string, unknown>>(props: T, schema: Partial<Record<keyof T, z.ZodTypeAny>>): Promise<void> {
|
||||
const schemaObj = z.object(
|
||||
Object.entries(schema).reduce((acc, [key, value]) => ({
|
||||
...acc,
|
||||
[key]: value
|
||||
}), {})
|
||||
)
|
||||
|
||||
try {
|
||||
await schemaObj.parseAsync(props)
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
const errors = error.issues.reduce((acc, err) => {
|
||||
const path = err.path.join('.')
|
||||
return {
|
||||
...acc,
|
||||
[path]: err.message
|
||||
}
|
||||
}, {})
|
||||
throw new Error(JSON.stringify({ errors }, null, 2))
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user