n8n-nodes-clay
Version:
n8n community node for Clay - Data enrichment and automation platform
540 lines (516 loc) • 25.6 kB
JavaScript
const { NodeConnectionType, NodeOperationError } = require("n8n-workflow");
const { clayWebhookRequest, validateWebhookUrl, validateAndSanitizeRecordData } = require("../generic.functions");
class ClayApi {
constructor() {
this.description = {
displayName: 'Clay',
name: 'clayApi',
icon: 'file:clay.png',
group: ['transform'],
version: 1,
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Interact with Clay data enrichment platform',
defaults: {
name: 'Clay',
},
inputs: [NodeConnectionType.Main],
outputs: [NodeConnectionType.Main],
credentials: [
{
name: 'clayApi',
required: true,
},
],
properties: [
{
displayName: 'Resource',
name: 'resource',
type: 'options',
noDataExpression: true,
options: [
{
name: 'Table',
value: 'table',
},
{
name: 'Workspace',
value: 'workspace',
},
],
default: 'table',
},
// TABLE OPERATIONS
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['table'],
},
},
options: [
{
name: 'Create Record',
value: 'createRecord',
description: 'Create a new record in a Clay table',
action: 'Create a record in table',
},
{
name: 'Update Record (Upsert)',
value: 'updateRecord',
description: 'Create or update a record using auto-dedupe (requires unique identifier)',
action: 'Update/create a record in table',
},
{
name: 'Find Record',
value: 'findRecord',
description: 'Find records in a Clay table',
action: 'Find records in table',
},
{
name: 'Get Schema',
value: 'getSchema',
description: 'Get table schema and field definitions',
action: 'Get table schema',
},
],
default: 'createRecord',
},
// WORKSPACE OPERATIONS
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['workspace'],
},
},
options: [
{
name: 'List Workspaces',
value: 'listWorkspaces',
description: 'List all available workspaces',
action: 'List workspaces',
},
{
name: 'List Tables',
value: 'listTables',
description: 'List tables in a workspace',
action: 'List tables in workspace',
},
],
default: 'listWorkspaces',
},
// WORKSPACE ID FIELD
{
displayName: 'Workspace ID',
name: 'workspaceId',
type: 'string',
required: true,
displayOptions: {
show: {
resource: ['table'],
},
},
default: '',
description: 'The ID of the Clay workspace',
placeholder: 'e.g. 12345',
},
{
displayName: 'Workspace ID',
name: 'workspaceId',
type: 'string',
required: true,
displayOptions: {
show: {
resource: ['workspace'],
operation: ['listTables'],
},
},
default: '',
description: 'The ID of the Clay workspace',
placeholder: 'e.g. 12345',
},
// TABLE ID FIELD
{
displayName: 'Table ID',
name: 'tableId',
type: 'string',
required: true,
displayOptions: {
show: {
resource: ['table'],
operation: ['createRecord', 'updateRecord', 'findRecord', 'getSchema'],
},
},
default: '',
description: 'The ID of the Clay table',
placeholder: 'e.g. 67890',
},
// WEBHOOK URL FIELD (for create and update record)
{
displayName: 'Webhook URL',
name: 'webhookUrl',
type: 'string',
required: true,
displayOptions: {
show: {
resource: ['table'],
operation: ['createRecord', 'updateRecord'],
},
},
default: '',
description: 'The Clay webhook URL for this table (from Clay table settings)',
placeholder: 'https://api.clay.com/v3/sources/webhook/pull-in-data-from-a-webhook-{UUID}',
},
// UNIQUE IDENTIFIER FIELD (for update record)
{
displayName: 'Unique Identifier Field',
name: 'uniqueField',
type: 'string',
required: true,
displayOptions: {
show: {
resource: ['table'],
operation: ['updateRecord'],
},
},
default: 'Email',
description: 'The field name used as unique identifier for auto-dedupe (e.g., Email, LinkedIn URL, Company Domain)',
placeholder: 'e.g. Email, LinkedIn URL, Company Domain',
},
// AUTO-DEDUPE WARNING
{
displayName: 'Important Note',
name: 'updateNote',
type: 'notice',
default: '',
displayOptions: {
show: {
resource: ['table'],
operation: ['updateRecord'],
},
},
typeOptions: {
theme: 'warning',
},
description: 'Clay\'s auto-dedupe feature must be enabled on your table with the unique identifier field. This operation will create a new record if no match is found, or rely on auto-dedupe to prevent duplicates.',
},
// FIELD MAPPING MODE FOR CREATE AND UPDATE RECORD
{
displayName: 'Field Mapping Mode',
name: 'fieldMappingMode',
type: 'options',
displayOptions: {
show: {
resource: ['table'],
operation: ['createRecord', 'updateRecord'],
},
},
options: [
{
name: 'Manual Field Mapping',
value: 'manual',
description: 'Manually specify field names and values',
},
{
name: 'JSON Object',
value: 'json',
description: 'Provide fields as a JSON object',
},
],
default: 'manual',
description: 'How to specify the fields for the new record',
},
// FIELDS FOR CREATE AND UPDATE RECORD (MANUAL MODE)
{
displayName: 'Fields',
name: 'fields',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
displayOptions: {
show: {
resource: ['table'],
operation: ['createRecord', 'updateRecord'],
fieldMappingMode: ['manual'],
},
},
default: {},
options: [
{
name: 'field',
displayName: 'Field',
values: [
{
displayName: 'Field Name',
name: 'name',
type: 'string',
default: '',
description: 'Name of the field in the Clay table (case-sensitive)',
placeholder: 'e.g. First Name, Email, Company',
},
{
displayName: 'Field Value',
name: 'value',
type: 'string',
default: '',
description: 'Value to set for this field',
placeholder: 'e.g. John Doe, john@example.com',
},
],
},
],
description: 'Fields to set in the new record. Field names must match exactly with Clay table column names.',
},
// JSON FIELDS FOR CREATE AND UPDATE RECORD (JSON MODE)
{
displayName: 'Fields (JSON)',
name: 'fieldsJson',
type: 'json',
displayOptions: {
show: {
resource: ['table'],
operation: ['createRecord', 'updateRecord'],
fieldMappingMode: ['json'],
},
},
default: '{\n "First Name": "John",\n "Last Name": "Doe",\n "Email": "john.doe@example.com",\n "Company": "Example Corp"\n}',
description: 'Fields as a JSON object. Keys must match Clay table column names exactly.',
typeOptions: {
rows: 10,
},
},
// SEARCH FIELD FOR FIND RECORD
{
displayName: 'Search Field',
name: 'searchField',
type: 'string',
required: true,
displayOptions: {
show: {
resource: ['table'],
operation: ['findRecord'],
},
},
default: '',
description: 'The field name to search by',
placeholder: 'e.g. Email',
},
// SEARCH VALUE FOR FIND RECORD
{
displayName: 'Search Value',
name: 'searchValue',
type: 'string',
required: true,
displayOptions: {
show: {
resource: ['table'],
operation: ['findRecord'],
},
},
default: '',
description: 'The value to search for',
placeholder: 'e.g. john.doe@example.com',
},
// LIMIT FOR FIND RECORD
{
displayName: 'Limit',
name: 'limit',
type: 'number',
typeOptions: {
minValue: 1,
maxValue: 100,
},
default: 10,
displayOptions: {
show: {
resource: ['table'],
operation: ['findRecord'],
},
},
description: 'Maximum number of records to return',
},
],
};
}
async execute() {
const items = this.getInputData();
const returnData = [];
for (let i = 0; i < items.length; i++) {
try {
const resource = this.getNodeParameter('resource', i);
const operation = this.getNodeParameter('operation', i);
let responseData;
if (resource === 'table') {
if (operation === 'createRecord' || operation === 'updateRecord') {
const webhookUrl = this.getNodeParameter('webhookUrl', i);
const fieldMappingMode = this.getNodeParameter('fieldMappingMode', i);
// Validate webhook URL format
if (!validateWebhookUrl(webhookUrl)) {
throw new NodeOperationError(this.getNode(), 'Invalid Clay webhook URL format. Expected format: https://api.clay.com/v3/sources/webhook/pull-in-data-from-a-webhook-{UUID}', { itemIndex: i });
}
// Build the record data based on mapping mode
let recordData = {};
if (fieldMappingMode === 'manual') {
const fields = this.getNodeParameter('fields', i);
if (fields.field && Array.isArray(fields.field)) {
for (const field of fields.field) {
if (field.name && field.value !== undefined) {
recordData[field.name] = field.value;
}
}
}
} else if (fieldMappingMode === 'json') {
const fieldsJson = this.getNodeParameter('fieldsJson', i);
try {
recordData = JSON.parse(fieldsJson);
} catch (error) {
throw new NodeOperationError(this.getNode(), `Invalid JSON in Fields (JSON): ${error.message}`, { itemIndex: i });
}
}
// For update operations, validate unique identifier is included
if (operation === 'updateRecord') {
const uniqueField = this.getNodeParameter('uniqueField', i);
if (!recordData[uniqueField]) {
throw new NodeOperationError(this.getNode(), `Unique identifier field "${uniqueField}" must be included in the record data for update operations.`, { itemIndex: i });
}
}
// Validate that we have some data to send
if (Object.keys(recordData).length === 0) {
throw new NodeOperationError(this.getNode(), `No fields specified for record ${operation === 'updateRecord' ? 'update' : 'creation'}. Please add at least one field.`, { itemIndex: i });
}
// Validate and sanitize the record data
try {
recordData = validateAndSanitizeRecordData(recordData);
} catch (error) {
throw new NodeOperationError(this.getNode(), `Invalid record data: ${error.message}`, { itemIndex: i });
}
// Send data to Clay webhook
try {
responseData = await clayWebhookRequest.call(this, webhookUrl, recordData);
// If the webhook doesn't return data, create a success response
if (!responseData) {
const operationType = operation === 'updateRecord' ? 'updated/created' : 'created';
responseData = {
success: true,
message: `Record ${operationType} successfully in Clay table`,
operation: operation,
data: recordData,
timestamp: new Date().toISOString(),
...(operation === 'updateRecord' && {
note: 'Record was sent to Clay with auto-dedupe. If a matching record exists, it may have been deduplicated rather than updated.',
}),
};
}
} catch (error) {
const operationType = operation === 'updateRecord' ? 'update/create' : 'create';
throw new NodeOperationError(this.getNode(), `Failed to ${operationType} record in Clay table: ${error.message}. Please verify the webhook URL is correct and the table is accessible.`, { itemIndex: i });
}
} else if (operation === 'findRecord') {
// Clay doesn't provide a direct API for finding records
// This operation provides guidance on alternatives
const searchField = this.getNodeParameter('searchField', i);
const searchValue = this.getNodeParameter('searchValue', i);
const limit = this.getNodeParameter('limit', i);
// Return informational response about Clay's limitations
responseData = {
success: false,
operation: 'findRecord',
message: 'Clay API does not support direct record search operations',
searchCriteria: {
field: searchField,
value: searchValue,
limit: limit,
},
alternatives: [
'Use Clay\'s built-in lookup functionality within Clay tables',
'Export table data and search externally',
'Use Clay\'s Zapier integration for search operations',
'Implement client-side filtering after data export',
],
documentation: 'https://www.clay.com/university/guide/lookup-data-from-other-tables',
timestamp: new Date().toISOString(),
};
} else if (operation === 'getSchema') {
// Clay doesn't provide a direct API for getting table schemas
// This operation provides guidance on alternatives
const workspaceId = this.getNodeParameter('workspaceId', i);
const tableId = this.getNodeParameter('tableId', i);
// Return informational response about Clay's limitations
responseData = {
success: false,
operation: 'getSchema',
message: 'Clay API does not support direct table schema retrieval',
tableInfo: {
workspaceId: workspaceId,
tableId: tableId,
},
alternatives: [
'View table schema within Clay interface',
'Export a sample record to see field structure',
'Use Clay\'s field mapping in webhook setup',
'Document field names manually for consistent usage',
],
recommendations: [
'Common Clay fields: First Name, Last Name, Email, Company, LinkedIn URL',
'Field names are case-sensitive in webhook operations',
'Use consistent field naming across your Clay tables',
],
documentation: 'https://www.clay.com/university/guide/http-api-integration-overview',
timestamp: new Date().toISOString(),
};
}
} else if (resource === 'workspace') {
if (operation === 'listWorkspaces') {
// Clay doesn't provide a direct API for listing workspaces
responseData = {
success: false,
operation: 'listWorkspaces',
message: 'Clay API does not support listing workspaces',
alternatives: [
'Find workspace IDs in Clay interface URL (e.g., app.clay.com/workspace/12345)',
'Check Clay table settings for workspace information',
'Use Clay\'s sharing features to get workspace details',
],
documentation: 'https://www.clay.com/university/guide/workspace-management',
timestamp: new Date().toISOString(),
};
} else if (operation === 'listTables') {
const workspaceId = this.getNodeParameter('workspaceId', i);
// Clay doesn't provide a direct API for listing tables
responseData = {
success: false,
operation: 'listTables',
message: 'Clay API does not support listing tables in workspace',
workspaceId: workspaceId,
alternatives: [
'Find table IDs in Clay interface URL (e.g., app.clay.com/workspace/12345/table/67890)',
'Check webhook URLs for table IDs',
'Use Clay\'s table sharing features to get table information',
],
documentation: 'https://www.clay.com/university/guide/table-management',
timestamp: new Date().toISOString(),
};
}
}
const executionData = this.helpers.constructExecutionMetaData(this.helpers.returnJsonArray(responseData), { itemData: { item: i } });
returnData.push(...executionData);
} catch (error) {
if (this.continueOnFail()) {
const executionData = this.helpers.constructExecutionMetaData([{ json: { error: error.message } }], { itemData: { item: i } });
returnData.push(...executionData);
continue;
}
throw error;
}
}
return [returnData];
}
}
module.exports = { ClayApi };