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,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}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -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}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -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}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -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}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -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,
|
||||
});
|
||||
@@ -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}`
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user