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,40 @@
{
"API Key": "API-Schlüssel",
"Application ID": "Anwendungs-ID",
"Your Knack API Key available in the Settings section of the Builder.": "Dein Knack-API-Schlüssel ist in den Einstellungen des Builders verfügbar.",
"Your Application ID available in the Settings section of the Builder.": "Ihre Anwendungs-ID ist in den Einstellungen des Builders verfügbar.",
"Create Record": "Datensatz erstellen",
"Delete Record": "Datensatz löschen",
"Find Record": "Datensatz finden",
"Update Record": "Datensatz aktualisieren",
"Custom API Call": "Eigener API-Aufruf",
"Creates a new record into a specified object/table.": "Erstellt einen neuen Datensatz in einem angegebenen Objekt/Tabelle.",
"Deletes an existing record from a table.": "Löscht einen vorhandenen Datensatz aus einer Tabelle.",
"Finds a single record using field value.": "Findet einen einzigen Datensatz mit Feldwert.",
"Updates an existing record.": "Aktualisiert einen existierenden Datensatz.",
"Make a custom API call to a specific endpoint": "Einen benutzerdefinierten API-Aufruf an einen bestimmten Endpunkt machen",
"Object": "Objekt",
"Record Fields": "Datensatzfelder",
"Record ID": "Datensatz-ID",
"Field ID": "Feld-ID",
"Field Value": "Feldwert",
"Method": "Methode",
"Headers": "Kopfzeilen",
"Query Parameters": "Abfrageparameter",
"Body": "Körper",
"Response is Binary ?": "Antwort ist binär?",
"No Error on Failure": "Kein Fehler bei Fehler",
"Timeout (in seconds)": "Timeout (in Sekunden)",
"The ID of the record to delete.": "Die ID des zu löschenden Datensatzes.",
"Field to find the record by": "Feld um den Datensatz zu finden von",
"The value to search for in the specified field.": "Der zu suchende Wert im angegebenen Feld.",
"The ID of the record to update.": "Die ID des zu aktualisierenden Datensatzes.",
"Authorization headers are injected automatically from your connection.": "Autorisierungs-Header werden automatisch von Ihrer Verbindung injiziert.",
"Enable for files like PDFs, images, etc..": "Aktivieren für Dateien wie PDFs, Bilder, etc..",
"GET": "ERHALTEN",
"POST": "POST",
"PATCH": "PATCH",
"PUT": "PUT",
"DELETE": "LÖSCHEN",
"HEAD": "HEAD"
}

View File

@@ -0,0 +1,40 @@
{
"API Key": "Clave API",
"Application ID": "ID de la aplicación",
"Your Knack API Key available in the Settings section of the Builder.": "Tu Clave de API de Knack disponible en la sección Configuración del Constructor.",
"Your Application ID available in the Settings section of the Builder.": "Su ID de aplicación está disponible en la sección Configuración del Constructor.",
"Create Record": "Crear registro",
"Delete Record": "Eliminar registro",
"Find Record": "Buscar registro",
"Update Record": "Actualizar registro",
"Custom API Call": "Llamada API personalizada",
"Creates a new record into a specified object/table.": "Crea un nuevo registro en un objeto/tabla especificado.",
"Deletes an existing record from a table.": "Elimina un registro existente de una tabla.",
"Finds a single record using field value.": "Encuentra un único registro usando un valor de campo.",
"Updates an existing record.": "Actualiza un registro existente.",
"Make a custom API call to a specific endpoint": "Hacer una llamada API personalizada a un extremo específico",
"Object": "Objeto",
"Record Fields": "Campos de registro",
"Record ID": "ID de registro",
"Field ID": "ID del campo",
"Field Value": "Valor del campo",
"Method": "Método",
"Headers": "Encabezados",
"Query Parameters": "Parámetros de consulta",
"Body": "Cuerpo",
"Response is Binary ?": "¿Respuesta es binaria?",
"No Error on Failure": "No hay ningún error en fallo",
"Timeout (in seconds)": "Tiempo de espera (en segundos)",
"The ID of the record to delete.": "El ID del registro a eliminar.",
"Field to find the record by": "Campo para encontrar el registro por",
"The value to search for in the specified field.": "El valor por el que buscar en el campo especificado.",
"The ID of the record to update.": "El ID del registro a actualizar.",
"Authorization headers are injected automatically from your connection.": "Las cabeceras de autorización se inyectan automáticamente desde tu conexión.",
"Enable for files like PDFs, images, etc..": "Activar para archivos como PDFs, imágenes, etc.",
"GET": "RECOGER",
"POST": "POST",
"PATCH": "PATCH",
"PUT": "PUT",
"DELETE": "BORRAR",
"HEAD": "LIMPIO"
}

View File

@@ -0,0 +1,40 @@
{
"API Key": "Clé API",
"Application ID": "ID de l'application",
"Your Knack API Key available in the Settings section of the Builder.": "Votre clé d'API Knack disponible dans la section Paramètres du Constructeur.",
"Your Application ID available in the Settings section of the Builder.": "Votre ID d'application disponible dans la section Paramètres du Constructeur.",
"Create Record": "Créer un enregistrement",
"Delete Record": "Supprimer l'enregistrement",
"Find Record": "Rechercher un enregistrement",
"Update Record": "Mettre à jour l'enregistrement",
"Custom API Call": "Appel API personnalisé",
"Creates a new record into a specified object/table.": "Crée un nouvel enregistrement dans un objet/table spécifié.",
"Deletes an existing record from a table.": "Supprime un enregistrement existant dans une table.",
"Finds a single record using field value.": "Trouve un enregistrement unique en utilisant la valeur du champ.",
"Updates an existing record.": "Met à jour un enregistrement existant.",
"Make a custom API call to a specific endpoint": "Passez un appel API personnalisé à un point de terminaison spécifique",
"Object": "Objet",
"Record Fields": "Champs d'enregistrement",
"Record ID": "ID de l'enregistrement",
"Field ID": "ID du champ",
"Field Value": "Valeur du champ",
"Method": "Méthode",
"Headers": "En-têtes",
"Query Parameters": "Paramètres de requête",
"Body": "Corps",
"Response is Binary ?": "La réponse est Binaire ?",
"No Error on Failure": "Aucune erreur en cas d'échec",
"Timeout (in seconds)": "Délai d'attente (en secondes)",
"The ID of the record to delete.": "L'ID de l'enregistrement à supprimer.",
"Field to find the record by": "Champ de recherche de l'enregistrement par",
"The value to search for in the specified field.": "La valeur à rechercher dans le champ spécifié.",
"The ID of the record to update.": "L'ID de l'enregistrement à mettre à jour.",
"Authorization headers are injected automatically from your connection.": "Les en-têtes d'autorisation sont injectés automatiquement à partir de votre connexion.",
"Enable for files like PDFs, images, etc..": "Activer pour les fichiers comme les PDFs, les images, etc.",
"GET": "OBTENIR",
"POST": "POSTER",
"PATCH": "PATCH",
"PUT": "EFFACER",
"DELETE": "SUPPRIMER",
"HEAD": "TÊTE"
}

View File

@@ -0,0 +1,40 @@
{
"API Key": "API キー",
"Application ID": "アプリケーション ID",
"Your Knack API Key available in the Settings section of the Builder.": "Knack API Keyは、ビルダーの設定セクションで使用できます。",
"Your Application ID available in the Settings section of the Builder.": "アプリケーションIDはビルダーのformat@@0セクションで使用できます。",
"Create Record": "レコードを作成",
"Delete Record": "レコードを削除",
"Find Record": "レコードを検索",
"Update Record": "更新記録",
"Custom API Call": "カスタムAPI通話",
"Creates a new record into a specified object/table.": "指定したオブジェクト/テーブルに新しいレコードを作成します。",
"Deletes an existing record from a table.": "テーブルから既存のレコードを削除します。",
"Finds a single record using field value.": "フィールド値を使用して単一のレコードを検索します。",
"Updates an existing record.": "既存のレコードを更新します。",
"Make a custom API call to a specific endpoint": "特定のエンドポイントへのカスタム API コールを実行します。",
"Object": "オブジェクト",
"Record Fields": "レコードフィールド",
"Record ID": "レコードID",
"Field ID": "フィールドID",
"Field Value": "フィールド値",
"Method": "方法",
"Headers": "ヘッダー",
"Query Parameters": "クエリパラメータ",
"Body": "本文",
"Response is Binary ?": "応答はバイナリですか?",
"No Error on Failure": "失敗時にエラーはありません",
"Timeout (in seconds)": "タイムアウト(秒)",
"The ID of the record to delete.": "削除するレコードのID。",
"Field to find the record by": "レコードを見つけるためのフィールド",
"The value to search for in the specified field.": "指定したフィールドで検索する値。",
"The ID of the record to update.": "更新するレコードのID。",
"Authorization headers are injected automatically from your connection.": "認証ヘッダは接続から自動的に注入されます。",
"Enable for files like PDFs, images, etc..": "PDF、画像などのファイルを有効にします。",
"GET": "取得",
"POST": "POST",
"PATCH": "PATCH",
"PUT": "PUT",
"DELETE": "削除",
"HEAD": "頭"
}

View File

@@ -0,0 +1,40 @@
{
"API Key": "API Sleutel",
"Application ID": "Applicatie ID",
"Your Knack API Key available in the Settings section of the Builder.": "Je Knack API-sleutel is beschikbaar in de Instellingensectie van de Builder.",
"Your Application ID available in the Settings section of the Builder.": "Uw toepassings-ID is beschikbaar in Instellingen gedeelte van de Builder.",
"Create Record": "Record Maken",
"Delete Record": "Record verwijderen",
"Find Record": "Vind Record",
"Update Record": "Update Record",
"Custom API Call": "Custom API Call",
"Creates a new record into a specified object/table.": "Maakt een nieuw record aan in een opgegeven object/tabel.",
"Deletes an existing record from a table.": "Verwijdert een bestaand record van een tabel.",
"Finds a single record using field value.": "Vindt een enkele record met behulp van veldwaarde.",
"Updates an existing record.": "Werkt een bestaand record bij.",
"Make a custom API call to a specific endpoint": "Maak een aangepaste API call naar een specifiek eindpunt",
"Object": "Object",
"Record Fields": "Record velden",
"Record ID": "Record ID",
"Field ID": "Veld ID",
"Field Value": "Veld waarde",
"Method": "Methode",
"Headers": "Kopteksten",
"Query Parameters": "Query parameters",
"Body": "Lichaam",
"Response is Binary ?": "Antwoord is binair?",
"No Error on Failure": "Geen fout bij fout",
"Timeout (in seconds)": "Time-out (in seconden)",
"The ID of the record to delete.": "Het ID van het te verwijderen record",
"Field to find the record by": "Veld om het record te vinden door",
"The value to search for in the specified field.": "De waarde waarnaar gezocht moet worden in het opgegeven veld.",
"The ID of the record to update.": "Het ID van het te updaten record",
"Authorization headers are injected automatically from your connection.": "Autorisatie headers worden automatisch geïnjecteerd vanuit uw verbinding.",
"Enable for files like PDFs, images, etc..": "Inschakelen voor bestanden zoals PDF's, afbeeldingen etc..",
"GET": "KRIJG",
"POST": "POSTE",
"PATCH": "BEKIJK",
"PUT": "PUT",
"DELETE": "VERWIJDEREN",
"HEAD": "HOOFD"
}

View File

@@ -0,0 +1,40 @@
{
"API Key": "Chave de API",
"Application ID": "ID da aplicação",
"Your Knack API Key available in the Settings section of the Builder.": "Sua chave de API Knack disponível na seção Configurações do Construtor.",
"Your Application ID available in the Settings section of the Builder.": "Seu ID de aplicativo disponível na seção Configurações do Construtor.",
"Create Record": "Criar Registro",
"Delete Record": "Excluir registro",
"Find Record": "Localizar Registro",
"Update Record": "Atualizar Registro",
"Custom API Call": "Chamada de API personalizada",
"Creates a new record into a specified object/table.": "Cria um novo registro em um objeto/tabela especificada.",
"Deletes an existing record from a table.": "Exclui um registro existente de uma tabela.",
"Finds a single record using field value.": "Encontra um único registro usando o valor do campo.",
"Updates an existing record.": "Atualiza um registro existente.",
"Make a custom API call to a specific endpoint": "Faça uma chamada de API personalizada para um ponto de extremidade específico",
"Object": "Objeto",
"Record Fields": "Recorde Campos",
"Record ID": "ID do Registro",
"Field ID": "ID do campo",
"Field Value": "Valor do Campo",
"Method": "Método",
"Headers": "Cabeçalhos",
"Query Parameters": "Parâmetros da consulta",
"Body": "Conteúdo",
"Response is Binary ?": "A resposta é binária ?",
"No Error on Failure": "Nenhum erro no Failure",
"Timeout (in seconds)": "Tempo limite (em segundos)",
"The ID of the record to delete.": "A ID do registro a excluir.",
"Field to find the record by": "Campo para encontrar o registro de",
"The value to search for in the specified field.": "O valor a procurar no campo especificado.",
"The ID of the record to update.": "A ID do registro a ser atualizada.",
"Authorization headers are injected automatically from your connection.": "Os cabeçalhos de autorização são inseridos automaticamente a partir da sua conexão.",
"Enable for files like PDFs, images, etc..": "Habilitar para arquivos como PDFs, imagens, etc..",
"GET": "OBTER",
"POST": "POSTAR",
"PATCH": "COMPRAR",
"PUT": "COLOCAR",
"DELETE": "EXCLUIR",
"HEAD": "CABEÇA"
}

View File

@@ -0,0 +1,39 @@
{
"Knack": "Кнак",
"API Key": "Ключ API",
"Application ID": "ID приложения",
"Your Knack API Key available in the Settings section of the Builder.": "Ваш ключ API Knack доступен в разделе Параметры конструктора.",
"Your Application ID available in the Settings section of the Builder.": "Ваш ID приложения доступен в разделе Настройки конструктора.",
"Create Record": "Создать запись",
"Delete Record": "Удалить запись",
"Find Record": "Найти запись",
"Update Record": "Обновить запись",
"Custom API Call": "Пользовательский вызов API",
"Creates a new record into a specified object/table.": "Создает новую запись в указанном объекте/таблице.",
"Deletes an existing record from a table.": "Удалить существующую запись из таблицы.",
"Finds a single record using field value.": "Ищет одну запись, используя значение поля.",
"Updates an existing record.": "Обновление существующей записи.",
"Make a custom API call to a specific endpoint": "Сделать пользовательский API вызов к определенной конечной точке",
"Object": "Объект",
"Record Fields": "Поля записи",
"Record ID": "ID записи",
"Field ID": "ID поля",
"Field Value": "Значение поля",
"Method": "Метод",
"Headers": "Заголовки",
"Query Parameters": "Параметры запроса",
"Body": "Тело",
"No Error on Failure": "Нет ошибок при ошибке",
"Timeout (in seconds)": "Таймаут (в секундах)",
"The ID of the record to delete.": "ID записи для удаления.",
"Field to find the record by": "Поле для поиска записи",
"The value to search for in the specified field.": "Значение для поиска в указанном поле.",
"The ID of the record to update.": "ID записи для обновления.",
"Authorization headers are injected automatically from your connection.": "Заголовки авторизации включаются автоматически из вашего соединения.",
"GET": "ПОЛУЧИТЬ",
"POST": "ПОСТ",
"PATCH": "ПАТЧ",
"PUT": "ПОКУПИТЬ",
"DELETE": "УДАЛИТЬ",
"HEAD": "HEAD"
}

View File

@@ -0,0 +1,40 @@
{
"API Key": "API Key",
"Application ID": "Application ID",
"Your Knack API Key available in the Settings section of the Builder.": "Your Knack API Key available in the Settings section of the Builder.",
"Your Application ID available in the Settings section of the Builder.": "Your Application ID available in the Settings section of the Builder.",
"Create Record": "Create Record",
"Delete Record": "Delete Record",
"Find Record": "Find Record",
"Update Record": "Update Record",
"Custom API Call": "Custom API Call",
"Creates a new record into a specified object/table.": "Creates a new record into a specified object/table.",
"Deletes an existing record from a table.": "Deletes an existing record from a table.",
"Finds a single record using field value.": "Finds a single record using field value.",
"Updates an existing record.": "Updates an existing record.",
"Make a custom API call to a specific endpoint": "Make a custom API call to a specific endpoint",
"Object": "Object",
"Record Fields": "Record Fields",
"Record ID": "Record ID",
"Field ID": "Field ID",
"Field Value": "Field Value",
"Method": "Method",
"Headers": "Headers",
"Query Parameters": "Query Parameters",
"Body": "Body",
"Response is Binary ?": "Response is Binary ?",
"No Error on Failure": "No Error on Failure",
"Timeout (in seconds)": "Timeout (in seconds)",
"The ID of the record to delete.": "The ID of the record to delete.",
"Field to find the record by": "Field to find the record by",
"The value to search for in the specified field.": "The value to search for in the specified field.",
"The ID of the record to update.": "The ID of the record to update.",
"Authorization headers are injected automatically from your connection.": "Authorization headers are injected automatically from your connection.",
"Enable for files like PDFs, images, etc..": "Enable for files like PDFs, images, etc..",
"GET": "GET",
"POST": "POST",
"PATCH": "PATCH",
"PUT": "PUT",
"DELETE": "DELETE",
"HEAD": "HEAD"
}

View File

@@ -0,0 +1,39 @@
{
"Knack": "Knack",
"API Key": "API Key",
"Application ID": "Application ID",
"Your Knack API Key available in the Settings section of the Builder.": "Your Knack API Key available in the Settings section of the Builder.",
"Your Application ID available in the Settings section of the Builder.": "Your Application ID available in the Settings section of the Builder.",
"Create Record": "Create Record",
"Delete Record": "Delete Record",
"Find Record": "Find Record",
"Update Record": "Update Record",
"Custom API Call": "Custom API Call",
"Creates a new record into a specified object/table.": "Creates a new record into a specified object/table.",
"Deletes an existing record from a table.": "Deletes an existing record from a table.",
"Finds a single record using field value.": "Finds a single record using field value.",
"Updates an existing record.": "Updates an existing record.",
"Make a custom API call to a specific endpoint": "Make a custom API call to a specific endpoint",
"Object": "Object",
"Record Fields": "Record Fields",
"Record ID": "Record ID",
"Field ID": "Field ID",
"Field Value": "Field Value",
"Method": "Method",
"Headers": "Headers",
"Query Parameters": "Query Parameters",
"Body": "Body",
"No Error on Failure": "No Error on Failure",
"Timeout (in seconds)": "Timeout (in seconds)",
"The ID of the record to delete.": "The ID of the record to delete.",
"Field to find the record by": "Field to find the record by",
"The value to search for in the specified field.": "The value to search for in the specified field.",
"The ID of the record to update.": "The ID of the record to update.",
"Authorization headers are injected automatically from your connection.": "Authorization headers are injected automatically from your connection.",
"GET": "GET",
"POST": "POST",
"PATCH": "PATCH",
"PUT": "PUT",
"DELETE": "DELETE",
"HEAD": "HEAD"
}

View File

@@ -0,0 +1,40 @@
{
"API Key": "API 密钥",
"Application ID": "Application ID",
"Your Knack API Key available in the Settings section of the Builder.": "Your Knack API Key available in the Settings section of the Builder.",
"Your Application ID available in the Settings section of the Builder.": "Your Application ID available in the Settings section of the Builder.",
"Create Record": "Create Record",
"Delete Record": "Delete Record",
"Find Record": "Find Record",
"Update Record": "Update Record",
"Custom API Call": "自定义 API 呼叫",
"Creates a new record into a specified object/table.": "Creates a new record into a specified object/table.",
"Deletes an existing record from a table.": "Deletes an existing record from a table.",
"Finds a single record using field value.": "Finds a single record using field value.",
"Updates an existing record.": "Updates an existing record.",
"Make a custom API call to a specific endpoint": "将一个自定义 API 调用到一个特定的终点",
"Object": "Object",
"Record Fields": "Record Fields",
"Record ID": "Record ID",
"Field ID": "Field ID",
"Field Value": "Field Value",
"Method": "方法",
"Headers": "信头",
"Query Parameters": "查询参数",
"Body": "正文内容",
"Response is Binary ?": "Response is Binary ?",
"No Error on Failure": "失败时没有错误",
"Timeout (in seconds)": "超时(秒)",
"The ID of the record to delete.": "The ID of the record to delete.",
"Field to find the record by": "Field to find the record by",
"The value to search for in the specified field.": "The value to search for in the specified field.",
"The ID of the record to update.": "The ID of the record to update.",
"Authorization headers are injected automatically from your connection.": "授权头自动从您的连接中注入。",
"Enable for files like PDFs, images, etc..": "Enable for files like PDFs, images, etc..",
"GET": "获取",
"POST": "帖子",
"PATCH": "PATCH",
"PUT": "弹出",
"DELETE": "删除",
"HEAD": "黑色"
}

View File

@@ -0,0 +1,35 @@
import { createCustomApiCallAction } from '@activepieces/pieces-common';
import { createPiece } from '@activepieces/pieces-framework';
import { knackAuth } from './lib/common/auth';
import { createRecordAction } from './lib/actions/create-record';
import { deleteRecordAction } from './lib/actions/delete-record';
import { findRecordAction } from './lib/actions/find-record';
import { updateRecordAction } from './lib/actions/update-record';
import { PieceCategory } from '@activepieces/shared';
export const knack = createPiece({
displayName: 'Knack',
auth: knackAuth,
minimumSupportedRelease: '0.20.0',
logoUrl: 'https://cdn.activepieces.com/pieces/knack.png',
categories:[PieceCategory.CONTENT_AND_FILES],
authors: ['aryel780'],
actions: [
createRecordAction,
deleteRecordAction,
findRecordAction,
updateRecordAction,
createCustomApiCallAction({
auth: knackAuth,
baseUrl: () => 'https://api.knack.com/v1',
authMapping: async (auth) => {
const { apiKey, applicationId } = auth.props;
return {
'X-Knack-Application-ID': applicationId,
'X-Knack-REST-API-Key': apiKey,
};
},
}),
],
triggers: [],
});

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