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:
poduck
2025-12-18 22:59:37 -05:00
parent 9848268d34
commit 3aa7199503
16292 changed files with 1284892 additions and 4708 deletions

View File

@@ -0,0 +1,5 @@
export * from './lib/authentication';
export * from './lib/helpers';
export * from './lib/http';
export * from './lib/polling';
export * from './lib/validation';

View File

@@ -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;
};

View File

@@ -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);
};

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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;
};

View File

@@ -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();

View File

@@ -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,
};
}
}

View File

@@ -0,0 +1,6 @@
export enum HttpHeader {
AUTHORIZATION = 'Authorization',
ACCEPT = 'Accept',
API_KEY = 'x-api-key',
CONTENT_TYPE = 'Content-Type',
}

View File

@@ -0,0 +1 @@
export type HttpHeaders = Record<string, string | string[] | undefined>;

View File

@@ -0,0 +1 @@
export type HttpMessageBody = any

View File

@@ -0,0 +1,8 @@
export enum HttpMethod {
GET = 'GET',
POST = 'POST',
PATCH = 'PATCH',
PUT = 'PUT',
DELETE = 'DELETE',
HEAD = 'HEAD',
}

View File

@@ -0,0 +1,3 @@
import { HttpMessageBody } from "./http-message-body";
export type HttpRequestBody = HttpMessageBody | string;

View File

@@ -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';
};

View File

@@ -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;
};

View File

@@ -0,0 +1,4 @@
export enum MediaType {
APPLICATION_JSON = 'application/json',
TEXT_CSV = 'text/csv',
}

View File

@@ -0,0 +1 @@
export type QueryParams = Record<string, string>;

View File

@@ -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';

View File

@@ -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);
}
}

View File

@@ -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
}
}
}