Add Autosend node with Mail and Contact resources

Implement n8n node for Autosend API with support for:
- Mail resource: Send single and bulk emails with template or custom content
- Contact resource: Create/update contacts (upsert) and get contacts by ID or email
- API key authentication
- Declarative routing following n8n best practices
- Full TypeScript support with proper typing
- Passing all linting checks
This commit is contained in:
Claude 2025-11-11 17:56:01 +00:00
commit 2f1ffde4a5
No known key found for this signature in database
15 changed files with 1142 additions and 7 deletions

View file

@ -0,0 +1,80 @@
import type { INodeProperties } from 'n8n-workflow';
export const getOperation: INodeProperties[] = [
{
displayName: 'Search By',
name: 'searchBy',
type: 'options',
required: true,
default: 'email',
options: [
{
name: 'Email',
value: 'email',
description: 'Search contact by email address',
},
{
name: 'ID',
value: 'id',
description: 'Search contact by ID',
},
],
displayOptions: {
show: {
resource: ['contact'],
operation: ['get'],
},
},
description: 'How to search for the contact',
},
{
displayName: 'Contact ID',
name: 'contactId',
type: 'string',
required: true,
default: '',
displayOptions: {
show: {
resource: ['contact'],
operation: ['get'],
searchBy: ['id'],
},
},
routing: {
request: {
url: '=/contacts/{{$parameter.contactId}}',
},
},
description: 'The ID of the contact to retrieve',
},
{
displayName: 'Email',
name: 'email',
type: 'string',
required: true,
placeholder: 'contact@example.com',
default: '',
displayOptions: {
show: {
resource: ['contact'],
operation: ['get'],
searchBy: ['email'],
},
},
routing: {
send: {
type: 'body',
property: 'emails',
preSend: [
async function (this, requestOptions) {
const email = this.getNodeParameter('email') as string;
requestOptions.body = requestOptions.body || {};
(requestOptions.body as Record<string, unknown>).emails = [email];
return requestOptions;
},
],
},
},
description: 'The email address of the contact to search for',
},
];

View file

@ -0,0 +1,64 @@
import type { INodeProperties } from 'n8n-workflow';
import { upsertOperation } from './upsert';
import { getOperation } from './get';
export const contactOperations: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['contact'],
},
},
options: [
{
name: 'Create or Update',
value: 'upsert',
description: 'Create a new record, or update the current one if it already exists (upsert)',
action: 'Create or update a contact',
routing: {
request: {
method: 'POST' as const,
url: '/contacts/email',
},
},
},
{
name: 'Get',
value: 'get',
description: 'Get a contact by ID or email',
action: 'Get a contact',
routing: {
request: {
method: 'GET' as const,
url: '/contacts/{{$parameter.contactId}}',
},
},
},
],
default: 'upsert',
},
];
export const contactFields: INodeProperties[] = [
...upsertOperation,
...getOperation.map((field) => {
// Override routing for email search to use the search endpoint
if (field.name === 'email' && field.routing) {
return {
...field,
routing: {
...field.routing,
request: {
method: 'POST' as const,
url: '/contacts/search/emails',
},
},
} as INodeProperties;
}
return field;
}),
];

View file

@ -0,0 +1,142 @@
import type { INodeProperties } from 'n8n-workflow';
export const upsertOperation: INodeProperties[] = [
{
displayName: 'Email',
name: 'email',
type: 'string',
required: true,
placeholder: 'contact@example.com',
default: '',
displayOptions: {
show: {
resource: ['contact'],
operation: ['upsert'],
},
},
routing: {
send: {
type: 'body',
property: 'email',
},
},
description: 'The email address of the contact',
},
{
displayName: 'User ID',
name: 'userId',
type: 'string',
default: '',
displayOptions: {
show: {
resource: ['contact'],
operation: ['upsert'],
},
},
routing: {
send: {
type: 'body',
property: 'userId',
},
},
description: 'External user ID from your system',
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: ['contact'],
operation: ['upsert'],
},
},
options: [
{
displayName: 'Company',
name: 'company',
type: 'string',
default: '',
routing: {
send: {
type: 'body',
property: 'company',
},
},
description: 'Company name',
},
{
displayName: 'Custom Fields (JSON)',
name: 'customFields',
type: 'json',
default: '{}',
routing: {
send: {
type: 'body',
property: 'customFields',
preSend: [
async function (this, requestOptions) {
const customFieldsStr = this.getNodeParameter(
'additionalFields.customFields',
'',
) as string;
if (customFieldsStr) {
try {
const customFields = JSON.parse(customFieldsStr);
requestOptions.body = requestOptions.body || {};
(requestOptions.body as Record<string, unknown>).customFields = customFields;
} catch {
throw new Error('Custom Fields must be valid JSON');
}
}
return requestOptions;
},
],
},
},
description: 'Additional custom fields as a JSON object',
},
{
displayName: 'First Name',
name: 'firstName',
type: 'string',
default: '',
routing: {
send: {
type: 'body',
property: 'firstName',
},
},
description: 'First name of the contact',
},
{
displayName: 'Last Name',
name: 'lastName',
type: 'string',
default: '',
routing: {
send: {
type: 'body',
property: 'lastName',
},
},
description: 'Last name of the contact',
},
{
displayName: 'Phone',
name: 'phone',
type: 'string',
default: '',
routing: {
send: {
type: 'body',
property: 'phone',
},
},
description: 'Phone number of the contact',
},
],
},
];