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,69 @@
import { HttpMethod } from '@activepieces/pieces-common';
import { createAction } from '@activepieces/pieces-framework';
import { knackApiCall } from '../common/client';
import { knackAuth } from '../common/auth';
import {
KnackGetObjectResponse,
knackTransformFields,
objectDropdown,
recordFields,
} from '../common/props';
export const createRecordAction = createAction({
auth: knackAuth,
name: 'create_record',
displayName: 'Create Record',
description: 'Creates a new record into a specified object/table.',
props: {
object: objectDropdown,
recordFields: recordFields,
},
async run({ propsValue, auth }) {
const { object: objectKey, recordFields: recordData } = propsValue;
try {
const response = await knackApiCall<Record<string, any>>({
method: HttpMethod.POST,
auth: auth,
resourceUri: `/objects/${objectKey}/records`,
body: recordData,
});
const objectDetails = await knackApiCall<KnackGetObjectResponse>({
method: HttpMethod.GET,
auth,
resourceUri: `/objects/${objectKey}`,
});
const transformedRecord = knackTransformFields(objectDetails, response);
return transformedRecord;
} catch (error: any) {
if (error.message.includes('409')) {
throw new Error(
'Conflict: The record could not be created due to a conflict, such as a duplicate unique value.'
);
}
if (error.message.includes('400')) {
throw new Error(
'Bad Request: Invalid request parameters. Please check your Record Data JSON and field values.'
);
}
if (error.message.includes('401') || error.message.includes('403')) {
throw new Error(
'Authentication Failed: Please check your API Key, Application ID, and user permissions.'
);
}
if (error.message.includes('429')) {
throw new Error(
'Rate Limit Exceeded: Too many requests. Please wait a moment before trying again.'
);
}
throw new Error(`Failed to create Knack record: ${error.message}`);
}
},
});

View File

@@ -0,0 +1,58 @@
import { HttpMethod } from '@activepieces/pieces-common';
import { createAction, Property } from '@activepieces/pieces-framework';
import { knackApiCall, KnackAuthProps } from '../common/client';
import { knackAuth } from '../common/auth';
import { objectDropdown } from '../common/props';
export const deleteRecordAction = createAction({
auth: knackAuth,
name: 'delete_record',
displayName: 'Delete Record',
description: 'Deletes an existing record from a table.',
props: {
object: objectDropdown,
recordId: Property.ShortText({
displayName: 'Record ID',
required: true,
description: 'The ID of the record to delete.',
}),
},
async run({ propsValue, auth }) {
const { object: objectKey, recordId } = propsValue;
try {
const response = await knackApiCall({
method: HttpMethod.DELETE,
auth: auth,
resourceUri: `/objects/${objectKey}/records/${recordId}`,
});
return response;
} catch (error: any) {
if (error.message.includes('404')) {
throw new Error(
'Not Found: The record ID was not found in the specified object. Please verify the ID is correct.'
);
}
if (error.message.includes('400')) {
throw new Error(
'Bad Request: The request was invalid. Please ensure the Record ID is formatted correctly.'
);
}
if (error.message.includes('401') || error.message.includes('403')) {
throw new Error(
'Authentication Failed: Please check your API Key, Application ID, and user permissions.'
);
}
if (error.message.includes('429')) {
throw new Error(
'Rate Limit Exceeded: Too many requests. Please wait a moment before trying again.'
);
}
throw new Error(`Failed to delete Knack record: ${error.message}`);
}
},
});

View File

@@ -0,0 +1,90 @@
import { HttpMethod } from '@activepieces/pieces-common';
import { createAction, Property } from '@activepieces/pieces-framework';
import { knackApiCall } from '../common/client';
import { knackAuth } from '../common/auth';
import {
fieldIdDropdown,
KnackGetObjectResponse,
knackTransformFields,
objectDropdown,
} from '../common/props';
export const findRecordAction = createAction({
auth: knackAuth,
name: 'find_record',
displayName: 'Find Record',
description: 'Finds a single record using field value.',
props: {
object: objectDropdown,
fieldId: fieldIdDropdown,
fieldValue: Property.ShortText({
displayName: 'Field Value',
required: true,
description: 'The value to search for in the specified field.',
}),
},
async run({ propsValue, auth }) {
const { object: objectKey, fieldId, fieldValue } = propsValue;
try {
const response = await knackApiCall<{ records: Record<string, any>[] }>({
method: HttpMethod.GET,
auth: auth,
resourceUri: `/objects/${objectKey}/records`,
query: {
filters: JSON.stringify([
{
field: fieldId,
operator: 'is',
value: fieldValue,
},
]),
rows_per_page: '1',
},
});
if (response.records && response.records.length > 0) {
const objectDetails = await knackApiCall<KnackGetObjectResponse>({
method: HttpMethod.GET,
auth,
resourceUri: `/objects/${objectKey}`,
});
const transformedRecord = knackTransformFields(
objectDetails,
response.records[0]
);
return {
found: true,
record: transformedRecord,
};
}
return {
found: false,
record: null,
message: 'No record found matching the provided filters.',
};
} catch (error: any) {
if (error.message.includes('400')) {
throw new Error(
'Bad Request: The filter rules are invalid. Please check the JSON format and field/operator values.'
);
}
if (error.message.includes('401') || error.message.includes('403')) {
throw new Error(
'Authentication Failed: Please check your API Key, Application ID, and user permissions.'
);
}
if (error.message.includes('429')) {
throw new Error(
'Rate Limit Exceeded: Too many requests. Please wait a moment before trying again.'
);
}
throw new Error(`Failed to find Knack record: ${error.message}`);
}
},
});

View File

@@ -0,0 +1,79 @@
import { HttpMethod } from '@activepieces/pieces-common';
import { createAction, Property } from '@activepieces/pieces-framework';
import { knackApiCall } from '../common/client';
import { knackAuth } from '../common/auth';
import {
KnackGetObjectResponse,
knackTransformFields,
objectDropdown,
recordFields,
} from '../common/props';
export const updateRecordAction = createAction({
auth: knackAuth,
name: 'update_record',
displayName: 'Update Record',
description: 'Updates an existing record.',
props: {
object: objectDropdown,
recordId: Property.ShortText({
displayName: 'Record ID',
required: true,
description: 'The ID of the record to update.',
}),
recordFields: recordFields,
},
async run({ propsValue, auth }) {
const { object: objectKey, recordId, recordFields } = propsValue;
try {
const response = await knackApiCall<Record<string, any>>({
method: HttpMethod.PUT,
auth: auth,
resourceUri: `/objects/${objectKey}/records/${recordId}`,
body: recordFields,
});
const objectDetails = await knackApiCall<KnackGetObjectResponse>({
method: HttpMethod.GET,
auth,
resourceUri: `/objects/${objectKey}`,
});
const transformedRecord = knackTransformFields(objectDetails, response);
return transformedRecord;
} catch (error: any) {
if (error.message.includes('404')) {
throw new Error(
'Not Found: The record ID was not found in the specified object. Please verify the ID is correct.'
);
}
if (error.message.includes('409')) {
throw new Error(
'Conflict: The record could not be updated due to a conflict, such as a duplicate unique value.'
);
}
if (error.message.includes('400')) {
throw new Error(
'Bad Request: Invalid request parameters. Please check your Data to Update JSON and field values.'
);
}
if (error.message.includes('401') || error.message.includes('403')) {
throw new Error(
'Authentication Failed: Please check your API Key, Application ID, and user permissions.'
);
}
if (error.message.includes('429')) {
throw new Error(
'Rate Limit Exceeded: Too many requests. Please wait a moment before trying again.'
);
}
throw new Error(`Failed to update Knack record: ${error.message}`);
}
},
});

View File

@@ -0,0 +1,38 @@
import { PieceAuth, Property } from '@activepieces/pieces-framework';
import { knackApiCall } from './client';
import { HttpMethod } from '@activepieces/pieces-common';
import { AppConnectionType } from '@activepieces/shared';
export const knackAuth = PieceAuth.CustomAuth({
props: {
apiKey: PieceAuth.SecretText({
displayName: 'API Key',
description: 'Your Knack API Key available in the Settings section of the Builder.',
required: true,
}),
applicationId: Property.ShortText({
displayName: 'Application ID',
description: 'Your Application ID available in the Settings section of the Builder.',
required: true,
}),
},
validate: async ({ auth }) => {
try {
await knackApiCall({
method: HttpMethod.GET,
auth: {
type: AppConnectionType.CUSTOM_AUTH,
props: auth,
},
resourceUri: '/objects',
});
return { valid: true };
} catch (e) {
return {
valid: false,
error: 'Invalid API Key or Application ID',
};
}
},
required: true,
});

View File

@@ -0,0 +1,120 @@
import {
httpClient,
HttpMethod,
HttpRequest,
HttpMessageBody,
QueryParams,
} from '@activepieces/pieces-common';
import { AppConnectionValueForAuthProperty } from '@activepieces/pieces-framework';
import { knackAuth } from './auth';
export type KnackAuthProps = {
apiKey: string;
applicationId: string;
};
export type KnackApiCallParams = {
method: HttpMethod;
resourceUri: string;
query?: Record<string, string | number | string[] | undefined>;
body?: any;
auth: AppConnectionValueForAuthProperty<typeof knackAuth>;
};
export async function knackApiCall<T extends HttpMessageBody>({
method,
resourceUri,
query,
body,
auth,
}: KnackApiCallParams): Promise<T> {
const { apiKey, applicationId } = auth.props;
if (!apiKey || !applicationId) {
throw new Error('Knack API key and Application ID are required for authentication');
}
const queryParams: QueryParams = {};
if (query) {
for (const [key, value] of Object.entries(query)) {
if (value !== null && value !== undefined) {
queryParams[key] = String(value);
}
}
}
const baseUrl = 'https://api.knack.com/v1';
const request: HttpRequest = {
method,
url: `${baseUrl}${resourceUri}`,
headers: {
'X-Knack-Application-ID': applicationId,
'X-Knack-REST-API-Key': apiKey,
'Content-Type': 'application/json',
},
queryParams,
body,
};
try {
const response = await httpClient.sendRequest<T>(request);
return response.body;
} catch (error: any) {
const statusCode = error.response?.status;
const errorData = error.response?.data;
switch (statusCode) {
case 400:
throw new Error(
`Bad Request: ${errorData?.message || 'Invalid request parameters'}. Please check your data and field values.`
);
case 401:
throw new Error(
'Authentication Failed: Invalid API key or Application ID. Please verify your Knack credentials in the connection settings.'
);
case 403:
throw new Error(
'Access Forbidden: You do not have permission to access this resource. Please check your Knack account permissions.'
);
case 404:
throw new Error(
'Resource Not Found: The requested object or resource does not exist. Please verify the identifier is correct.'
);
case 429:
throw new Error(
'Rate Limit Exceeded: Too many requests in a short time period. Please wait before trying again.'
);
case 500:
throw new Error(
'Internal Server Error: Knack is experiencing technical difficulties. Please try again later or contact Knack support.'
);
case 502:
case 503:
case 504:
throw new Error(
'Service Unavailable: Knack service is temporarily unavailable. Please try again in a few minutes.'
);
default:
{
const errorMessage = errorData?.message ||
error.message ||
'Unknown error occurred';
throw new Error(
`Knack API Error (${statusCode || 'Unknown'}): ${errorMessage}`
);
}
}
}
}

View File

@@ -0,0 +1,214 @@
import { Property, DynamicPropsValue } from '@activepieces/pieces-framework';
import { HttpMethod } from '@activepieces/pieces-common';
import { knackApiCall, KnackAuthProps } from './client';
import { knackAuth } from './auth';
interface KnackObject {
key: string;
name: string;
}
export interface KnackGetObjectResponse {
object: {
fields: {
type: string;
key: string;
name: string;
format: {
type: string;
options: string[];
};
}[];
};
}
export const objectDropdown = Property.Dropdown({
displayName: 'Object',
required: true,
refreshers: [],
auth: knackAuth,
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
placeholder: 'Please authenticate first.',
options: [],
};
}
const typedAuth = auth;
try {
const response = await knackApiCall<{ objects: KnackObject[] }>({
method: HttpMethod.GET,
auth: typedAuth,
resourceUri: '/objects',
});
return {
disabled: false,
options: response.objects.map((object) => ({
label: object.name,
value: object.key,
})),
};
} catch (error: any) {
return {
disabled: true,
placeholder: `Error loading objects: ${error.message}`,
options: [],
};
}
},
});
export const fieldIdDropdown = Property.Dropdown({
auth: knackAuth,
displayName: 'Field ID',
required: true,
description:'Field to find the record by',
refreshers: ['object'],
options: async ({ auth, object }) => {
if (!auth || !object) {
return {
disabled: true,
placeholder: 'Please select an object first.',
options: [],
};
}
const typedAuth = auth;
try {
const response = await knackApiCall<KnackGetObjectResponse>({
method: HttpMethod.GET,
auth: typedAuth,
resourceUri: `/objects/${object}`,
});
return {
disabled: false,
options: response.object.fields.map((field) => ({
label: field.name,
value: field.key,
})),
};
} catch (error: any) {
return {
disabled: true,
placeholder: `Error loading objects: ${error.message}`,
options: [],
};
}
},
});
export const recordFields = Property.DynamicProperties({
auth: knackAuth,
displayName: 'Record Fields',
refreshers: ['object'],
required: true,
props: async ({ auth, object }) => {
if (!auth || !object) {
return {};
}
const props: DynamicPropsValue = {};
const typedAuth = auth;
const response = await knackApiCall<KnackGetObjectResponse>({
method: HttpMethod.GET,
auth: typedAuth,
resourceUri: `/objects/${object}`,
});
for (const field of response.object.fields) {
switch (field.type) {
case 'short_text':
case 'email':
case 'phone':
case 'link':
props[field.key] = Property.ShortText({
displayName: field.name,
required: false,
});
break;
case 'paragraph_text':
case 'address':
case 'rich_text':
props[field.key] = Property.LongText({
displayName: field.name,
required: false,
});
break;
case 'number':
case 'rating':
case 'currency':
props[field.key] = Property.Number({
displayName: field.name,
required: false,
});
break;
case 'multiple_choice': {
const options = field.format.options.map((option) => ({
label: option,
value: option,
}));
props[field.key] =
field.format.type === 'multi'
? Property.StaticMultiSelectDropdown({
displayName: field.name,
required: false,
options: {
options,
},
})
: Property.StaticDropdown({
displayName: field.name,
required: false,
options: { options },
});
break;
}
case 'boolean':
props[field.key] = Property.Checkbox({
displayName: field.name,
required: false,
});
break;
}
}
return props;
},
});
export function knackTransformFields(
objectDetails: KnackGetObjectResponse,
recordFields: Record<string, any>
): Record<string, any> {
const fields = objectDetails.object.fields;
const keyToNameMap: Record<string, string> = {};
for (const field of fields) {
if (field && field.key && field.name) {
keyToNameMap[field.key] = field.name;
}
}
const transformed: Record<string, any> = {};
for (const [key, value] of Object.entries(recordFields)) {
const isRaw = key.endsWith('_raw');
const baseKey = isRaw ? key.slice(0, -4) : key;
const fieldName = keyToNameMap[baseKey];
if (fieldName) {
const transformedKey = isRaw ? `${fieldName} raw` : fieldName;
transformed[transformedKey] = value;
} else {
transformed[key] = value;
}
}
return transformed;
}