first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled

This commit is contained in:
2026-03-17 16:22:57 +03:30
commit 3d5eaf9445
15349 changed files with 2847338 additions and 0 deletions
@@ -0,0 +1,18 @@
{
"node": "n8n-nodes-base.awsLambda",
"nodeVersion": "1.0",
"codexVersion": "1.0",
"categories": ["Development"],
"resources": {
"credentialDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/credentials/aws/"
}
],
"primaryDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.awslambda/"
}
]
}
}
@@ -0,0 +1,219 @@
import type {
IExecuteFunctions,
IDataObject,
ILoadOptionsFunctions,
INodeExecutionData,
INodePropertyOptions,
INodeType,
INodeTypeDescription,
JsonObject,
} from 'n8n-workflow';
import { NodeConnectionTypes, NodeApiError } from 'n8n-workflow';
import { awsApiRequestREST } from './GenericFunctions';
import { awsNodeAuthOptions, awsNodeCredentials } from './utils';
export class AwsLambda implements INodeType {
description: INodeTypeDescription = {
displayName: 'AWS Lambda',
name: 'awsLambda',
icon: 'file:lambda.svg',
group: ['output'],
version: 1,
subtitle: '={{$parameter["function"]}}',
description: 'Invoke functions on AWS Lambda',
defaults: {
name: 'AWS Lambda',
},
usableAsTool: true,
inputs: [NodeConnectionTypes.Main],
outputs: [NodeConnectionTypes.Main],
credentials: awsNodeCredentials,
properties: [
awsNodeAuthOptions,
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
options: [
{
name: 'Invoke',
value: 'invoke',
description: 'Invoke a function',
action: 'Invoke a function',
},
],
default: 'invoke',
},
{
displayName: 'Function Name or ID',
name: 'function',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getFunctions',
},
displayOptions: {
show: {
operation: ['invoke'],
},
},
options: [],
default: '',
required: true,
description:
'The function you want to invoke. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
},
{
displayName: 'Qualifier',
name: 'qualifier',
type: 'string',
displayOptions: {
show: {
operation: ['invoke'],
},
},
required: true,
default: '$LATEST',
description: 'Specify a version or alias to invoke a published version of the function',
},
{
displayName: 'Invocation Type',
name: 'invocationType',
type: 'options',
options: [
{
name: 'Wait for Results',
value: 'RequestResponse',
description: 'Invoke the function synchronously and wait for the response',
},
{
name: 'Continue Workflow',
value: 'Event',
description: 'Invoke the function and immediately continue the workflow',
},
],
displayOptions: {
show: {
operation: ['invoke'],
},
},
default: 'RequestResponse',
description: 'Specify if the workflow should wait for the function to return the results',
},
{
displayName: 'JSON Input',
name: 'payload',
type: 'string',
displayOptions: {
show: {
operation: ['invoke'],
},
},
default: '',
description: 'The JSON that you want to provide to your Lambda function as input',
},
],
};
methods = {
loadOptions: {
async getFunctions(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const data = await awsApiRequestREST.call(this, 'lambda', 'GET', '/2015-03-31/functions/');
for (const func of data.Functions!) {
returnData.push({
name: func.FunctionName as string,
value: func.FunctionArn as string,
});
}
if (data.NextMarker) {
let marker: string = data.NextMarker;
while (true) {
const dataLoop = await awsApiRequestREST.call(
this,
'lambda',
'GET',
`/2015-03-31/functions/?MaxItems=50&Marker=${encodeURIComponent(marker)}`,
);
for (const func of dataLoop.Functions!) {
returnData.push({
name: func.FunctionName as string,
value: func.FunctionArn as string,
});
}
if (dataLoop.NextMarker) {
marker = dataLoop.NextMarker;
} else {
break;
}
}
}
return returnData;
},
},
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: IDataObject[] = [];
for (let i = 0; i < items.length; i++) {
try {
const params = {
FunctionName: this.getNodeParameter('function', i) as string,
InvocationType: this.getNodeParameter('invocationType', i) as string,
Payload: this.getNodeParameter('payload', i) as string,
Qualifier: this.getNodeParameter('qualifier', i) as string,
};
const responseData = await awsApiRequestREST.call(
this,
'lambda',
'POST',
`/2015-03-31/functions/${params.FunctionName}/invocations?Qualifier=${params.Qualifier}`,
params.Payload,
{
'X-Amz-Invocation-Type': params.InvocationType,
'Content-Type': 'application/x-amz-json-1.0',
},
);
if (responseData?.errorMessage !== undefined) {
let _errorMessage = responseData.errorMessage;
if (responseData.stackTrace) {
_errorMessage += `\n\nStack trace:\n${responseData.stackTrace}`;
}
throw new NodeApiError(this.getNode(), responseData as JsonObject);
} else {
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray({
result: responseData,
}),
{ itemData: { item: i } },
);
returnData.push(...executionData);
}
} catch (error) {
if (this.continueOnFail()) {
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray({ error: (error as JsonObject).message }),
{ itemData: { item: i } },
);
returnData.push(...executionData);
continue;
}
throw error;
}
}
return [this.helpers.returnJsonArray(returnData)];
}
}
@@ -0,0 +1,18 @@
{
"node": "n8n-nodes-base.awsSns",
"nodeVersion": "1.0",
"codexVersion": "1.0",
"categories": ["Development", "Communication"],
"resources": {
"credentialDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/credentials/aws/"
}
],
"primaryDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.awssns/"
}
]
}
}
@@ -0,0 +1,329 @@
import {
type IExecuteFunctions,
type IDataObject,
type ILoadOptionsFunctions,
type INodeExecutionData,
type INodeListSearchItems,
type INodeListSearchResult,
type INodeType,
type INodeTypeDescription,
NodeConnectionTypes,
} from 'n8n-workflow';
import { awsApiRequestSOAP } from './GenericFunctions';
import { awsNodeAuthOptions, awsNodeCredentials } from './utils';
export class AwsSns implements INodeType {
description: INodeTypeDescription = {
displayName: 'AWS SNS',
name: 'awsSns',
icon: 'file:sns.svg',
group: ['output'],
version: 1,
subtitle: '={{$parameter["topic"]}}',
description: 'Sends data to AWS SNS',
defaults: {
name: 'AWS SNS',
},
usableAsTool: true,
inputs: [NodeConnectionTypes.Main],
outputs: [NodeConnectionTypes.Main],
credentials: awsNodeCredentials,
properties: [
awsNodeAuthOptions,
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
options: [
{
name: 'Create',
value: 'create',
description: 'Create a topic',
action: 'Create a topic',
},
{
name: 'Delete',
value: 'delete',
description: 'Delete a topic',
action: 'Delete a topic',
},
{
name: 'Publish',
value: 'publish',
description: 'Publish a message to a topic',
action: 'Publish a message to a topic',
},
],
default: 'publish',
},
{
displayName: 'Name',
name: 'name',
type: 'string',
required: true,
default: '',
displayOptions: {
show: {
operation: ['create'],
},
},
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add option',
default: {},
options: [
{
displayName: 'Display Name',
name: 'displayName',
type: 'string',
default: '',
description: 'The display name to use for a topic with SMS subscriptions',
},
{
displayName: 'Fifo Topic',
name: 'fifoTopic',
type: 'boolean',
default: false,
description:
'Whether the topic you want to create is a FIFO (first-in-first-out) topic',
},
],
displayOptions: {
show: {
operation: ['create'],
},
},
},
{
displayName: 'Topic',
name: 'topic',
type: 'resourceLocator',
default: { mode: 'list', value: '' },
required: true,
modes: [
{
displayName: 'From List',
name: 'list',
type: 'list',
placeholder: 'Select a topic...',
typeOptions: {
searchListMethod: 'listTopics',
searchable: true,
},
},
{
displayName: 'By URL',
name: 'url',
type: 'string',
placeholder:
'https://us-east-1.console.aws.amazon.com/sns/v3/home?region=us-east-1#/topic/arn:aws:sns:us-east-1:777777777777:your_topic',
validation: [
{
type: 'regex',
properties: {
regex:
'https:\\/\\/[0-9a-zA-Z\\-_]+\\.console\\.aws\\.amazon\\.com\\/sns\\/v3\\/home\\?region\\=[0-9a-zA-Z\\-_]+\\#\\/topic\\/arn:aws:sns:[0-9a-zA-Z\\-_]+:[0-9]+:[0-9a-zA-Z\\-_]+(?:\\/.*|)',
errorMessage: 'Not a valid AWS SNS Topic URL',
},
},
],
extractValue: {
type: 'regex',
regex:
'https:\\/\\/[0-9a-zA-Z\\-_]+\\.console\\.aws\\.amazon\\.com\\/sns\\/v3\\/home\\?region\\=[0-9a-zA-Z\\-_]+\\#\\/topic\\/(arn:aws:sns:[0-9a-zA-Z\\-_]+:[0-9]+:[0-9a-zA-Z\\-_]+)(?:\\/.*|)',
},
},
{
displayName: 'ID',
name: 'id',
type: 'string',
validation: [
{
type: 'regex',
properties: {
regex: 'arn:aws:sns:[0-9a-zA-Z\\-_]+:[0-9]+:[0-9a-zA-Z\\-_]+',
errorMessage: 'Not a valid AWS SNS Topic ARN',
},
},
],
placeholder: 'arn:aws:sns:your-aws-region:777777777777:your_topic',
},
],
displayOptions: {
show: {
operation: ['publish', 'delete'],
},
},
},
{
displayName: 'Subject',
name: 'subject',
type: 'string',
displayOptions: {
show: {
operation: ['publish'],
},
},
default: '',
required: true,
description: 'Subject when the message is delivered to email endpoints',
},
{
displayName: 'Message',
name: 'message',
type: 'string',
displayOptions: {
show: {
operation: ['publish'],
},
},
required: true,
default: '',
description: 'The message you want to send',
},
],
};
methods = {
listSearch: {
async listTopics(
this: ILoadOptionsFunctions,
filter?: string,
paginationToken?: string,
): Promise<INodeListSearchResult> {
const returnData: INodeListSearchItems[] = [];
const params = paginationToken ? `NextToken=${encodeURIComponent(paginationToken)}` : '';
const data = await awsApiRequestSOAP.call(
this,
'sns',
'GET',
'/?Action=ListTopics&' + params,
);
let topics = data.ListTopicsResponse.ListTopicsResult.Topics.member;
const nextToken = data.ListTopicsResponse.ListTopicsResult.NextToken;
if (nextToken) {
paginationToken = nextToken as string;
} else {
paginationToken = undefined;
}
if (!Array.isArray(topics)) {
topics = [topics];
}
for (const topic of topics) {
const topicArn = topic.TopicArn as string;
const arnParsed = topicArn.split(':');
const topicName = arnParsed[5];
const awsRegion = arnParsed[3];
if (filter && !topicName.includes(filter)) {
continue;
}
returnData.push({
name: topicName,
value: topicArn,
url: `https://${awsRegion}.console.aws.amazon.com/sns/v3/home?region=${awsRegion}#/topic/${topicArn}`,
});
}
return { results: returnData, paginationToken };
},
},
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: IDataObject[] = [];
const operation = this.getNodeParameter('operation', 0);
for (let i = 0; i < items.length; i++) {
try {
if (operation === 'create') {
let name = this.getNodeParameter('name', i) as string;
const fifoTopic = this.getNodeParameter('options.fifoTopic', i, false) as boolean;
const displayName = this.getNodeParameter('options.displayName', i, '') as string;
const params: string[] = [];
if (fifoTopic && !name.endsWith('.fifo')) {
name = `${name}.fifo`;
}
params.push(`Name=${name}`);
if (fifoTopic) {
params.push('Attributes.entry.1.key=FifoTopic');
params.push('Attributes.entry.1.value=true');
}
if (displayName) {
params.push('Attributes.entry.2.key=DisplayName');
params.push(`Attributes.entry.2.value=${displayName}`);
}
const responseData = await awsApiRequestSOAP.call(
this,
'sns',
'GET',
'/?Action=CreateTopic&' + params.join('&'),
);
returnData.push({
TopicArn: responseData.CreateTopicResponse.CreateTopicResult.TopicArn,
} as IDataObject);
}
if (operation === 'delete') {
const topic = this.getNodeParameter('topic', i, undefined, {
extractValue: true,
}) as string;
const params = ['TopicArn=' + topic];
await awsApiRequestSOAP.call(
this,
'sns',
'GET',
'/?Action=DeleteTopic&' + params.join('&'),
);
// response of delete is the same no matter if topic was deleted or not
returnData.push({ success: true } as IDataObject);
}
if (operation === 'publish') {
const topic = this.getNodeParameter('topic', i, undefined, {
extractValue: true,
}) as string;
const params = [
'TopicArn=' + topic,
'Subject=' + encodeURIComponent(this.getNodeParameter('subject', i) as string),
'Message=' + encodeURIComponent(this.getNodeParameter('message', i) as string),
];
const responseData = await awsApiRequestSOAP.call(
this,
'sns',
'GET',
'/?Action=Publish&' + params.join('&'),
);
returnData.push({
MessageId: responseData.PublishResponse.PublishResult.MessageId,
} as IDataObject);
}
} catch (error) {
if (this.continueOnFail()) {
returnData.push({ error: error.message });
continue;
}
throw error;
}
}
return [this.helpers.returnJsonArray(returnData)];
}
}
@@ -0,0 +1,18 @@
{
"node": "n8n-nodes-base.awsSnsTrigger",
"nodeVersion": "1.0",
"codexVersion": "1.0",
"categories": ["Development", "Communication"],
"resources": {
"credentialDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/credentials/aws/"
}
],
"primaryDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/trigger-nodes/n8n-nodes-base.awssnstrigger/"
}
]
}
}
@@ -0,0 +1,276 @@
import get from 'lodash/get';
import type {
IHookFunctions,
IWebhookFunctions,
ILoadOptionsFunctions,
INodeListSearchItems,
INodeListSearchResult,
INodeType,
INodeTypeDescription,
IWebhookResponseData,
} from 'n8n-workflow';
import { jsonParse, NodeConnectionTypes, NodeOperationError } from 'n8n-workflow';
import { awsApiRequestSOAP } from './GenericFunctions';
import { awsNodeAuthOptions, awsNodeCredentials } from './utils';
export class AwsSnsTrigger implements INodeType {
description: INodeTypeDescription = {
displayName: 'AWS SNS Trigger',
subtitle: '={{$parameter["topic"].split(\':\')[5]}}',
name: 'awsSnsTrigger',
icon: 'file:sns.svg',
group: ['trigger'],
version: 1,
description: 'Handle AWS SNS events via webhooks',
defaults: {
name: 'AWS SNS Trigger',
},
inputs: [],
outputs: [NodeConnectionTypes.Main],
credentials: awsNodeCredentials,
webhooks: [
{
name: 'default',
httpMethod: 'POST',
responseMode: 'onReceived',
path: 'webhook',
},
],
properties: [
awsNodeAuthOptions,
{
displayName: 'Topic',
name: 'topic',
type: 'resourceLocator',
default: { mode: 'list', value: '' },
required: true,
modes: [
{
displayName: 'From List',
name: 'list',
type: 'list',
placeholder: 'Select a topic...',
typeOptions: {
searchListMethod: 'listTopics',
searchable: true,
},
},
{
displayName: 'By URL',
name: 'url',
type: 'string',
placeholder:
'https://us-east-1.console.aws.amazon.com/sns/v3/home?region=us-east-1#/topic/arn:aws:sns:us-east-1:777777777777:your_topic',
validation: [
{
type: 'regex',
properties: {
regex:
'https:\\/\\/[0-9a-zA-Z\\-_]+\\.console\\.aws\\.amazon\\.com\\/sns\\/v3\\/home\\?region\\=[0-9a-zA-Z\\-_]+\\#\\/topic\\/arn:aws:sns:[0-9a-zA-Z\\-_]+:[0-9]+:[0-9a-zA-Z\\-_]+(?:\\/.*|)',
errorMessage: 'Not a valid AWS SNS Topic URL',
},
},
],
extractValue: {
type: 'regex',
regex:
'https:\\/\\/[0-9a-zA-Z\\-_]+\\.console\\.aws\\.amazon\\.com\\/sns\\/v3\\/home\\?region\\=[0-9a-zA-Z\\-_]+\\#\\/topic\\/(arn:aws:sns:[0-9a-zA-Z\\-_]+:[0-9]+:[0-9a-zA-Z\\-_]+)(?:\\/.*|)',
},
},
{
displayName: 'ID',
name: 'id',
type: 'string',
validation: [
{
type: 'regex',
properties: {
regex: 'arn:aws:sns:[0-9a-zA-Z\\-_]+:[0-9]+:[0-9a-zA-Z\\-_]+',
errorMessage: 'Not a valid AWS SNS Topic ARN',
},
},
],
placeholder: 'arn:aws:sns:your-aws-region:777777777777:your_topic',
},
],
},
],
};
methods = {
listSearch: {
async listTopics(
this: ILoadOptionsFunctions,
filter?: string,
paginationToken?: string,
): Promise<INodeListSearchResult> {
const returnData: INodeListSearchItems[] = [];
const params = paginationToken ? `NextToken=${encodeURIComponent(paginationToken)}` : '';
const data = await awsApiRequestSOAP.call(
this,
'sns',
'GET',
'/?Action=ListTopics&' + params,
);
let topics = data.ListTopicsResponse.ListTopicsResult.Topics.member;
const nextToken = data.ListTopicsResponse.ListTopicsResult.NextToken;
if (nextToken) {
paginationToken = nextToken as string;
} else {
paginationToken = undefined;
}
if (!Array.isArray(topics)) {
topics = [topics];
}
for (const topic of topics) {
const topicArn = topic.TopicArn as string;
const arnParsed = topicArn.split(':');
const topicName = arnParsed[5];
const awsRegion = arnParsed[3];
if (filter && !topicName.includes(filter)) {
continue;
}
returnData.push({
name: topicName,
value: topicArn,
url: `https://${awsRegion}.console.aws.amazon.com/sns/v3/home?region=${awsRegion}#/topic/${topicArn}`,
});
}
return { results: returnData, paginationToken };
},
},
};
webhookMethods = {
default: {
async checkExists(this: IHookFunctions): Promise<boolean> {
const webhookData = this.getWorkflowStaticData('node');
const topic = this.getNodeParameter('topic', undefined, {
extractValue: true,
}) as string;
if (webhookData.webhookId === undefined) {
return false;
}
const params = [`TopicArn=${topic}`, 'Version=2010-03-31'];
const data = await awsApiRequestSOAP.call(
this,
'sns',
'GET',
'/?Action=ListSubscriptionsByTopic&' + params.join('&'),
);
const subscriptions = get(
data,
'ListSubscriptionsByTopicResponse.ListSubscriptionsByTopicResult.Subscriptions',
);
if (!subscriptions?.member) {
return false;
}
let subscriptionMembers = subscriptions.member;
if (!Array.isArray(subscriptionMembers)) {
subscriptionMembers = [subscriptionMembers];
}
for (const subscription of subscriptionMembers) {
if (webhookData.webhookId === subscription.SubscriptionArn) {
return true;
}
}
return false;
},
async create(this: IHookFunctions): Promise<boolean> {
const webhookData = this.getWorkflowStaticData('node');
const webhookUrl = this.getNodeWebhookUrl('default') as string;
const topic = this.getNodeParameter('topic', undefined, {
extractValue: true,
}) as string;
if (webhookUrl.includes('%20')) {
throw new NodeOperationError(
this.getNode(),
'The name of the SNS Trigger Node is not allowed to contain any spaces!',
);
}
const params = [
`TopicArn=${topic}`,
`Endpoint=${webhookUrl}`,
`Protocol=${webhookUrl?.split(':')[0]}`,
'ReturnSubscriptionArn=true',
'Version=2010-03-31',
];
const { SubscribeResponse } = await awsApiRequestSOAP.call(
this,
'sns',
'GET',
'/?Action=Subscribe&' + params.join('&'),
);
webhookData.webhookId = SubscribeResponse.SubscribeResult.SubscriptionArn;
return true;
},
async delete(this: IHookFunctions): Promise<boolean> {
const webhookData = this.getWorkflowStaticData('node');
const params = [`SubscriptionArn=${webhookData.webhookId}`, 'Version=2010-03-31'];
try {
await awsApiRequestSOAP.call(
this,
'sns',
'GET',
'/?Action=Unsubscribe&' + params.join('&'),
);
} catch (error) {
return false;
}
delete webhookData.webhookId;
return true;
},
},
};
async webhook(this: IWebhookFunctions): Promise<IWebhookResponseData> {
const req = this.getRequestObject();
const topic = this.getNodeParameter('topic', undefined, {
extractValue: true,
}) as string;
const body = jsonParse<{ Type: string; TopicArn: string; Token: string }>(
req.rawBody.toString(),
);
if (body.Type === 'SubscriptionConfirmation' && body.TopicArn === topic) {
const { Token } = body;
const params = [`TopicArn=${topic}`, `Token=${Token}`, 'Version=2010-03-31'];
await awsApiRequestSOAP.call(
this,
'sns',
'GET',
'/?Action=ConfirmSubscription&' + params.join('&'),
);
return {
noWebhookResponse: true,
};
}
if (body.Type === 'UnsubscribeConfirmation') {
return {};
}
//TODO verify message signature
return {
workflowData: [this.helpers.returnJsonArray(body)],
};
}
}
@@ -0,0 +1,29 @@
{
"node": "n8n-nodes-base.awsCertificateManager",
"nodeVersion": "1.0",
"codexVersion": "1.0",
"categories": ["Development"],
"resources": {
"credentialDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/credentials/aws/"
}
],
"primaryDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.awscertificatemanager/"
}
],
"generic": [
{
"label": "Why business process automation with n8n can change your daily life",
"icon": "🧬",
"url": "https://n8n.io/blog/why-business-process-automation-with-n8n-can-change-your-daily-life/"
},
{
"label": "7 no-code workflow automations for Amazon Web Services",
"url": "https://n8n.io/blog/aws-workflow-automation/"
}
]
}
}
@@ -0,0 +1,232 @@
import type {
IExecuteFunctions,
IDataObject,
INodeExecutionData,
INodeType,
INodeTypeDescription,
} from 'n8n-workflow';
import { NodeConnectionTypes } from 'n8n-workflow';
import { certificateFields, certificateOperations } from './CertificateDescription';
import { awsApiRequestAllItems, awsApiRequestREST } from './GenericFunctions';
import { awsNodeAuthOptions, awsNodeCredentials } from '../utils';
export class AwsCertificateManager implements INodeType {
description: INodeTypeDescription = {
displayName: 'AWS Certificate Manager',
name: 'awsCertificateManager',
icon: 'file:acm.svg',
group: ['output'],
version: 1,
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Sends data to AWS Certificate Manager',
defaults: {
name: 'AWS Certificate Manager',
},
inputs: [NodeConnectionTypes.Main],
outputs: [NodeConnectionTypes.Main],
credentials: awsNodeCredentials,
properties: [
awsNodeAuthOptions,
{
displayName: 'Resource',
name: 'resource',
type: 'options',
noDataExpression: true,
options: [
{
name: 'Certificate',
value: 'certificate',
},
],
default: 'certificate',
},
// Certificate
...certificateOperations,
...certificateFields,
],
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: IDataObject[] = [];
const qs: IDataObject = {};
let responseData;
const resource = this.getNodeParameter('resource', 0);
const operation = this.getNodeParameter('operation', 0);
for (let i = 0; i < items.length; i++) {
try {
if (resource === 'certificate') {
//https://docs.aws.amazon.com/acm/latest/APIReference/API_DeleteCertificate.html
if (operation === 'delete') {
const certificateArn = this.getNodeParameter('certificateArn', i) as string;
const body: IDataObject = {
CertificateArn: certificateArn,
};
responseData = await awsApiRequestREST.call(
this,
'acm',
'POST',
'',
JSON.stringify(body),
qs,
{
'X-Amz-Target': 'CertificateManager.DeleteCertificate',
'Content-Type': 'application/x-amz-json-1.1',
},
);
responseData = { success: true };
}
//https://docs.aws.amazon.com/acm/latest/APIReference/API_GetCertificate.html
if (operation === 'get') {
const certificateArn = this.getNodeParameter('certificateArn', i) as string;
const body: IDataObject = {
CertificateArn: certificateArn,
};
responseData = await awsApiRequestREST.call(
this,
'acm',
'POST',
'',
JSON.stringify(body),
qs,
{
'X-Amz-Target': 'CertificateManager.GetCertificate',
'Content-Type': 'application/x-amz-json-1.1',
},
);
}
//https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListObjectsV2.html
if (operation === 'getMany') {
const returnAll = this.getNodeParameter('returnAll', 0);
const options = this.getNodeParameter('options', i);
const body: { Includes: IDataObject; CertificateStatuses: string[]; MaxItems: number } =
{
CertificateStatuses: [],
Includes: {},
MaxItems: 0,
};
if (options.certificateStatuses) {
body.CertificateStatuses = options.certificateStatuses as string[];
}
if (options.certificateStatuses) {
body.Includes.extendedKeyUsage = options.extendedKeyUsage as string[];
}
if (options.keyTypes) {
body.Includes.keyTypes = options.keyTypes as string[];
}
if (options.keyUsage) {
body.Includes.keyUsage = options.keyUsage as string[];
}
if (returnAll) {
responseData = await awsApiRequestAllItems.call(
this,
'CertificateSummaryList',
'acm',
'POST',
'',
'{}',
qs,
{
'X-Amz-Target': 'CertificateManager.ListCertificates',
'Content-Type': 'application/x-amz-json-1.1',
},
);
} else {
body.MaxItems = this.getNodeParameter('limit', 0);
responseData = await awsApiRequestREST.call(
this,
'acm',
'POST',
'',
JSON.stringify(body),
qs,
{
'X-Amz-Target': 'CertificateManager.ListCertificates',
'Content-Type': 'application/x-amz-json-1.1',
},
);
responseData = responseData.CertificateSummaryList;
}
}
//https://docs.aws.amazon.com/acm/latest/APIReference/API_DescribeCertificate.html
if (operation === 'getMetadata') {
const certificateArn = this.getNodeParameter('certificateArn', i) as string;
const body: IDataObject = {
CertificateArn: certificateArn,
};
responseData = await awsApiRequestREST.call(
this,
'acm',
'POST',
'',
JSON.stringify(body),
qs,
{
'X-Amz-Target': 'CertificateManager.DescribeCertificate',
'Content-Type': 'application/x-amz-json-1.1',
},
);
responseData = responseData.Certificate;
}
//https://docs.aws.amazon.com/acm/latest/APIReference/API_RenewCertificate.html
if (operation === 'renew') {
const certificateArn = this.getNodeParameter('certificateArn', i) as string;
const body: IDataObject = {
CertificateArn: certificateArn,
};
responseData = await awsApiRequestREST.call(
this,
'acm',
'POST',
'',
JSON.stringify(body),
qs,
{
'X-Amz-Target': 'CertificateManager.RenewCertificate',
'Content-Type': 'application/x-amz-json-1.1',
},
);
responseData = { success: true };
}
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(responseData as IDataObject),
{ itemData: { item: i } },
);
returnData.push(...executionData);
}
} catch (error) {
if (this.continueOnFail()) {
returnData.push({ json: { error: error.message } });
continue;
}
throw error;
}
}
return [returnData as INodeExecutionData[]];
}
}
@@ -0,0 +1,327 @@
import type { INodeProperties } from 'n8n-workflow';
export const certificateOperations: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['certificate'],
},
},
options: [
{
name: 'Delete',
value: 'delete',
description: 'Delete a certificate',
action: 'Delete a certificate',
},
{
name: 'Get',
value: 'get',
description: 'Get a certificate',
action: 'Get a certificate',
},
{
name: 'Get Many',
value: 'getMany',
description: 'Get many certificates',
action: 'Get many certificates',
},
{
name: 'Get Metadata',
value: 'getMetadata',
description: 'Get certificate metadata',
action: 'Get certificate metadata',
},
{
name: 'Renew',
value: 'renew',
description: 'Renew a certificate',
action: 'Renew a certificate',
},
],
default: 'renew',
},
];
export const certificateFields: INodeProperties[] = [
/* -------------------------------------------------------------------------- */
/* certificate:renew */
/* -------------------------------------------------------------------------- */
{
displayName: 'Certificate ARN',
name: 'certificateArn',
type: 'string',
required: true,
default: '',
displayOptions: {
show: {
resource: ['certificate'],
operation: ['renew', 'get', 'delete', 'getMetadata'],
},
},
description:
'String that contains the ARN of the ACM certificate to be renewed. This must be of the form: arn:aws:acm:region:123456789012:certificate/12345678-1234-1234-1234-123456789012.',
},
/* -------------------------------------------------------------------------- */
/* certificate:delete */
/* -------------------------------------------------------------------------- */
{
displayName: 'Bucket Name',
name: 'bucketName',
type: 'string',
required: true,
default: '',
displayOptions: {
show: {
resource: ['certificate'],
operation: ['delete'],
},
},
},
{
displayName: 'Certificate Key',
name: 'certificateKey',
type: 'string',
required: true,
default: '',
displayOptions: {
show: {
resource: ['certificate'],
operation: ['delete'],
},
},
},
/* -------------------------------------------------------------------------- */
/* certificate:getMany */
/* -------------------------------------------------------------------------- */
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
operation: ['getMany'],
resource: ['certificate'],
},
},
default: false,
description: 'Whether to return all results or only up to a given limit',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
displayOptions: {
show: {
operation: ['getMany'],
resource: ['certificate'],
returnAll: [false],
},
},
typeOptions: {
minValue: 1,
maxValue: 500,
},
default: 100,
description: 'Max number of results to return',
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: ['certificate'],
operation: ['getMany'],
},
},
options: [
{
displayName: 'Certificate Statuses',
name: 'certificateStatuses',
type: 'multiOptions',
options: [
{
name: 'Expired',
value: 'EXPIRED',
},
{
name: 'Failed',
value: 'FAILED',
},
{
name: 'Inactive',
value: 'INACTIVE',
},
{
name: 'Issued',
value: 'ISSUED',
},
{
name: 'Pending Validation',
value: 'PENDING_VALIDATION',
},
{
name: 'Revoked',
value: 'REVOKED',
},
{
name: 'Validation Timed Out',
value: 'VALIDATION_TIMED_OUT',
},
],
default: [],
description: 'Filter the certificate list by status value',
},
{
displayName: 'Extended Key Usage',
name: 'extendedKeyUsage',
type: 'multiOptions',
options: [
{
name: 'Any',
value: 'ANY',
},
{
name: 'Code Signing',
value: 'CODE_SIGNING',
},
{
name: 'Custom',
value: 'CUSTOM',
},
{
name: 'Email Protection',
value: 'EMAIL_PROTECTION',
},
{
name: 'IPSEC End System',
value: 'IPSEC_END_SYSTEM',
},
{
name: 'IPSEC Tunnel',
value: 'IPSEC_TUNNEL',
},
{
name: 'IPSEC User',
value: 'IPSEC_USER',
},
{
name: 'None',
value: 'NONE',
},
{
name: 'OCSP Signing',
value: 'OCSP_SIGNING',
},
{
name: 'Time Stamping',
value: 'TIME_STAMPING',
},
{
name: 'TLS Web Client Authentication',
value: 'TLS_WEB_CLIENT_AUTHENTICATION',
},
{
name: 'TLS Web Server Authentication',
value: 'TLS_WEB_SERVER_AUTHENTICATION',
},
],
default: [],
description: 'Specify one or more ExtendedKeyUsage extension values',
},
{
displayName: 'Key Types',
name: 'keyTypes',
type: 'multiOptions',
options: [
{
name: 'EC Prime256v1',
value: 'EC_prime256v1',
},
{
name: 'EC Secp384r1',
value: 'EC_secp384r1',
},
{
name: 'EC Secp521r1',
value: 'EC_secp521r1',
},
{
name: 'RSA 1024',
value: 'RSA_1024',
},
{
name: 'RSA 2048',
value: 'RSA_2048',
},
{
name: 'RSA 4096',
value: 'RSA_4096',
},
],
default: ['RSA_2048'],
description: 'Specify one or more algorithms that can be used to generate key pairs',
},
{
displayName: 'Key Usage',
name: 'keyUsage',
type: 'multiOptions',
options: [
{
name: 'Any',
value: 'ANY',
},
{
name: 'Certificate Signing',
value: 'CERTIFICATE_SIGNING',
},
{
name: 'CRL Signing',
value: 'CRL_SIGNING',
},
{
name: 'Custom',
value: 'CUSTOM',
},
{
name: 'Data Encipherment',
value: 'DATA_ENCIPHERMENT',
},
{
name: 'Decipher Only',
value: 'DECIPHER_ONLY',
},
{
name: 'Digital Signature',
value: 'DIGITAL_SIGNATURE',
},
{
name: 'Encipher Only',
value: 'ENCIPHER_ONLY',
},
{
name: 'Key Agreement',
value: 'KEY_AGREEMENT',
},
{
name: 'Key Encipherment',
value: 'KEY_ENCIPHERMENT',
},
{
name: 'Non Repudiation',
value: 'NON_REPUDIATION',
},
],
default: [],
description: 'Specify one or more KeyUsage extension values',
},
],
},
];
@@ -0,0 +1,89 @@
import get from 'lodash/get';
import type {
IDataObject,
IExecuteFunctions,
IHookFunctions,
ILoadOptionsFunctions,
IWebhookFunctions,
IHttpRequestOptions,
JsonObject,
IHttpRequestMethods,
} from 'n8n-workflow';
import { jsonParse, NodeApiError } from 'n8n-workflow';
import { getAwsCredentials } from '../GenericFunctions';
export async function awsApiRequest(
this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions | IWebhookFunctions,
service: string,
method: IHttpRequestMethods,
path: string,
body?: string | Buffer,
query: IDataObject = {},
headers?: object,
): Promise<any> {
const { credentials, credentialsType } = await getAwsCredentials(this);
const requestOptions = {
qs: {
service,
path,
...query,
},
headers,
method,
url: '',
body,
region: credentials?.region as string,
} as IHttpRequestOptions;
try {
return await this.helpers.requestWithAuthentication.call(this, credentialsType, requestOptions);
} catch (error) {
throw new NodeApiError(this.getNode(), error as JsonObject);
}
}
export async function awsApiRequestREST(
this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions,
service: string,
method: IHttpRequestMethods,
path: string,
body?: string,
query: IDataObject = {},
headers?: object,
): Promise<any> {
const response = await awsApiRequest.call(this, service, method, path, body, query, headers);
try {
return JSON.parse(response as string);
} catch (e) {
return response;
}
}
export async function awsApiRequestAllItems(
this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions,
propertyName: string,
service: string,
method: IHttpRequestMethods,
path: string,
body?: string,
query: IDataObject = {},
headers: IDataObject = {},
): Promise<any> {
const returnData: IDataObject[] = [];
let responseData;
do {
responseData = await awsApiRequestREST.call(this, service, method, path, body, query, headers);
if (responseData.NextToken) {
const data = jsonParse<any>(body as string, {
errorMessage: 'Response body is not valid JSON',
});
data.NextToken = responseData.NextToken;
}
returnData.push.apply(returnData, get(responseData, propertyName) as IDataObject[]);
} while (responseData.NextToken !== undefined);
return returnData;
}
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#fff" fill-rule="evenodd" stroke="#000" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 85 85"><use xlink:href="#a" x="2.5" y="2.5"/><symbol id="a" overflow="visible"><g stroke="none"><path fill="#759c3e" d="m80 47.005-40.012 4.962V27.99L80 32.952z"/><path fill="#4b612c" d="m0 47.005 39.988 4.962V27.99L0 32.952z"/><path fill="#648339" d="M10 0h60v15H10z"/><path fill="#3c4929" d="m65 20-48.976.212L10 15h60z"/><path fill="#648339" d="M10 65h60v15H10z"/><path fill="#b7ca9d" d="M65 60H15l-5 5h60z"/></g></symbol></svg>

After

Width:  |  Height:  |  Size: 620 B

@@ -0,0 +1,18 @@
{
"node": "n8n-nodes-base.awsCognito",
"nodeVersion": "1.0",
"codexVersion": "1.0",
"categories": ["Development"],
"resources": {
"credentialDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/credentials/aws/"
}
],
"primaryDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.awscognito/"
}
]
}
}
@@ -0,0 +1,76 @@
import type { INodeType, INodeTypeDescription } from 'n8n-workflow';
import { NodeConnectionTypes } from 'n8n-workflow';
import { group, user, userPool } from './descriptions';
import { preSendStringifyBody } from './helpers/utils';
import { listSearch } from './methods';
export class AwsCognito implements INodeType {
description: INodeTypeDescription = {
displayName: 'AWS Cognito',
name: 'awsCognito',
icon: {
light: 'file:cognito.svg',
dark: 'file:cognito.svg',
},
group: ['output'],
version: 1,
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Sends data to AWS Cognito',
defaults: {
name: 'AWS Cognito',
},
inputs: [NodeConnectionTypes.Main],
outputs: [NodeConnectionTypes.Main],
credentials: [
{
name: 'aws',
required: true,
},
],
requestDefaults: {
headers: {
'Content-Type': 'application/x-amz-json-1.1',
},
qs: {
service: 'cognito-idp',
_region: '={{$credentials.region}}',
},
},
properties: [
{
displayName: 'Resource',
name: 'resource',
type: 'options',
noDataExpression: true,
default: 'user',
routing: {
send: {
preSend: [preSendStringifyBody],
},
},
options: [
{
name: 'Group',
value: 'group',
},
{
name: 'User',
value: 'user',
},
{
name: 'User Pool',
value: 'userPool',
},
],
},
...group.description,
...user.description,
...userPool.description,
],
};
methods = {
listSearch,
};
}
@@ -0,0 +1 @@
<svg width="2140" height="2500" viewBox="0 0 256 299" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid"><path d="M208.752 58.061l25.771-6.636.192.283.651 155.607-.843.846-5.31.227-20.159-3.138-.302-.794V58.061M59.705 218.971l.095.007 68.027 19.767.173.133.296.236-.096 59.232-.2.252-68.295-33.178v-46.449" fill="#7A3E65"/><path d="M208.752 204.456l-80.64 19.312-40.488-9.773-27.919 4.976L128 238.878l105.405-28.537 1.118-2.18-25.771-3.705" fill="#CFB2C1"/><path d="M196.295 79.626l-.657-.749-66.904-19.44-.734.283-.672-.343L22.052 89.734l-.575.703.845.463 24.075 3.53.851-.289 80.64-19.311 40.488 9.773 27.919-4.977" fill="#512843"/><path d="M47.248 240.537l-25.771 6.221-.045-.149-1.015-155.026 1.06-1.146 25.771 3.704v146.396" fill="#C17B9E"/><path d="M82.04 180.403l45.96 5.391.345-.515.187-71.887-.532-.589-45.96 5.392v62.208" fill="#7A3E65"/><path d="M173.96 180.403L128 185.794v-72.991l45.96 5.392v62.208M196.295 79.626L128 59.72V0l68.295 33.177v46.449" fill="#C17B9E"/><path d="M128 0L0 61.793v175.011l21.477 9.954V90.437L128 59.72V0" fill="#7A3E65"/><path d="M234.523 51.425v156.736L128 238.878v59.72l128-61.794V61.793l-21.477-10.368" fill="#C17B9E"/></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

@@ -0,0 +1,132 @@
import type { INodeProperties } from 'n8n-workflow';
export const userPoolResourceLocator: INodeProperties = {
displayName: 'User Pool',
name: 'userPool',
required: true,
type: 'resourceLocator',
default: {
mode: 'list',
value: '',
},
routing: {
send: {
type: 'body',
property: 'UserPoolId',
},
},
modes: [
{
displayName: 'From list',
name: 'list',
type: 'list',
typeOptions: {
searchListMethod: 'searchUserPools',
searchable: true,
},
},
{
displayName: 'By ID',
name: 'id',
type: 'string',
validation: [
{
type: 'regex',
properties: {
regex: '^[\\w-]+_[0-9a-zA-Z]+$',
errorMessage: 'The ID must follow the pattern "xxxxxx_xxxxxxxxxxx"',
},
},
],
placeholder: 'e.g. eu-central-1_ab12cdefgh',
},
],
};
export const groupResourceLocator: INodeProperties = {
displayName: 'Group',
name: 'group',
default: {
mode: 'list',
value: '',
},
routing: {
send: {
type: 'body',
property: 'GroupName',
},
},
modes: [
{
displayName: 'From list',
name: 'list',
type: 'list',
typeOptions: {
searchListMethod: 'searchGroups',
searchable: true,
},
},
{
displayName: 'By Name',
name: 'groupName',
type: 'string',
hint: 'Enter the group name',
validation: [
{
type: 'regex',
properties: {
regex: '^[\\w+=,.@-]+$',
errorMessage: 'The group name must follow the allowed pattern.',
},
},
],
placeholder: 'e.g. Admins',
},
],
required: true,
type: 'resourceLocator',
};
export const userResourceLocator: INodeProperties = {
displayName: 'User',
name: 'user',
default: {
mode: 'list',
value: '',
},
modes: [
{
displayName: 'From List',
name: 'list',
type: 'list',
typeOptions: {
searchListMethod: 'searchUsers',
searchable: true,
},
},
{
displayName: 'By ID',
name: 'id',
type: 'string',
hint: 'Enter the user ID',
placeholder: 'e.g. 02bd9fd6-8f93-4758-87c3-1fb73740a315',
validation: [
{
type: 'regex',
properties: {
regex: '^[\\w-]+-[0-9a-zA-Z]+$',
errorMessage: 'The ID must follow the pattern "xxxxxx-xxxxxxxxxxx"',
},
},
],
},
],
routing: {
send: {
type: 'body',
property: 'Username',
},
},
required: true,
type: 'resourceLocator',
};
@@ -0,0 +1,155 @@
import type { INodeProperties } from 'n8n-workflow';
import * as create from './create.operation';
import * as del from './delete.operation';
import * as get from './get.operation';
import * as getAll from './getAll.operation';
import * as update from './update.operation';
import { handleError } from '../../helpers/errorHandler';
import { processGroup } from '../../helpers/utils';
export const description: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
default: 'getAll',
displayOptions: {
show: {
resource: ['group'],
},
},
options: [
{
name: 'Create',
value: 'create',
description: 'Create a new group',
routing: {
request: {
method: 'POST',
headers: {
'X-Amz-Target': 'AWSCognitoIdentityProviderService.CreateGroup',
},
ignoreHttpStatusErrors: true,
},
output: {
postReceive: [
handleError,
{
type: 'rootProperty',
properties: {
property: 'Group',
},
},
],
},
},
action: 'Create group',
},
{
name: 'Delete',
value: 'delete',
description: 'Delete an existing group',
routing: {
request: {
method: 'POST',
headers: {
'X-Amz-Target': 'AWSCognitoIdentityProviderService.DeleteGroup',
},
ignoreHttpStatusErrors: true,
},
output: {
postReceive: [
handleError,
{
type: 'set',
properties: {
value: '={{ { "deleted": true } }}',
},
},
],
},
},
action: 'Delete group',
},
{
name: 'Get',
value: 'get',
description: 'Retrieve details of an existing group',
routing: {
request: {
method: 'POST',
headers: {
'X-Amz-Target': 'AWSCognitoIdentityProviderService.GetGroup',
},
ignoreHttpStatusErrors: true,
},
output: {
postReceive: [handleError, processGroup],
},
},
action: 'Get group',
},
{
name: 'Get Many',
value: 'getAll',
description: 'Retrieve a list of groups',
routing: {
request: {
method: 'POST',
headers: {
'X-Amz-Target': 'AWSCognitoIdentityProviderService.ListGroups',
},
ignoreHttpStatusErrors: true,
},
output: {
postReceive: [
handleError,
processGroup,
{
type: 'rootProperty',
properties: {
property: 'Groups',
},
},
],
},
},
action: 'Get many groups',
},
{
name: 'Update',
value: 'update',
description: 'Update an existing group',
routing: {
request: {
method: 'POST',
headers: {
'X-Amz-Target': 'AWSCognitoIdentityProviderService.UpdateGroup',
},
ignoreHttpStatusErrors: true,
},
output: {
postReceive: [
handleError,
{
type: 'set',
properties: {
value: '={{ { "updated": true } }}',
},
},
],
},
},
action: 'Update group',
},
],
},
...create.description,
...del.description,
...get.description,
...getAll.description,
...update.description,
];
@@ -0,0 +1,106 @@
import type { IExecuteSingleFunctions, IHttpRequestOptions, INodeProperties } from 'n8n-workflow';
import { NodeApiError, updateDisplayOptions } from 'n8n-workflow';
import { validateArn } from '../../helpers/utils';
import { userPoolResourceLocator } from '../common.description';
const properties: INodeProperties[] = [
{
...userPoolResourceLocator,
description: 'Select the user pool to use',
},
{
displayName: 'Group Name',
name: 'newGroupName',
default: '',
placeholder: 'e.g. MyNewGroup',
description: 'The name of the new group to create',
required: true,
type: 'string',
validateType: 'string',
routing: {
send: {
property: 'GroupName',
type: 'body',
preSend: [
async function (
this: IExecuteSingleFunctions,
requestOptions: IHttpRequestOptions,
): Promise<IHttpRequestOptions> {
const newGroupName = this.getNodeParameter('newGroupName', '') as string;
const groupNameRegex = /^[\p{L}\p{M}\p{S}\p{N}\p{P}]+$/u;
if (!groupNameRegex.test(newGroupName)) {
throw new NodeApiError(this.getNode(), {
message: 'Invalid format for Group Name',
description: 'Group Name should not contain spaces.',
});
}
return requestOptions;
},
],
},
},
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
default: {},
options: [
{
displayName: 'Description',
name: 'description',
default: '',
placeholder: 'e.g. New group description',
description: 'A description for the new group',
type: 'string',
routing: {
send: {
type: 'body',
property: 'Description',
},
},
},
{
displayName: 'Precedence',
name: 'precedence',
default: '',
placeholder: 'e.g. 10',
description: 'Precedence value for the group. Lower values indicate higher priority.',
type: 'number',
routing: {
send: {
type: 'body',
property: 'Precedence',
},
},
validateType: 'number',
},
{
displayName: 'Role ARN',
name: 'arn',
default: '',
placeholder: 'e.g. arn:aws:iam::123456789012:role/GroupRole',
description: 'The role ARN for the group, used for setting claims in tokens',
type: 'string',
routing: {
send: {
type: 'body',
property: 'Arn',
preSend: [validateArn],
},
},
},
],
placeholder: 'Add Option',
type: 'collection',
},
];
const displayOptions = {
show: {
resource: ['group'],
operation: ['create'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
@@ -0,0 +1,24 @@
import type { INodeProperties } from 'n8n-workflow';
import { updateDisplayOptions } from 'n8n-workflow';
import { groupResourceLocator, userPoolResourceLocator } from '../common.description';
const properties: INodeProperties[] = [
{
...userPoolResourceLocator,
description: 'Select the user pool to use',
},
{
...groupResourceLocator,
description: 'Select the group you want to delete',
},
];
const displayOptions = {
show: {
resource: ['group'],
operation: ['delete'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
@@ -0,0 +1,31 @@
import type { INodeProperties } from 'n8n-workflow';
import { updateDisplayOptions } from 'n8n-workflow';
import { groupResourceLocator, userPoolResourceLocator } from '../common.description';
const properties: INodeProperties[] = [
{
...userPoolResourceLocator,
description: 'Select the user pool to use',
},
{
...groupResourceLocator,
description: 'Select the group you want to retrieve',
},
{
displayName: 'Include Users',
name: 'includeUsers',
type: 'boolean',
default: false,
description: 'Whether to include a list of users in the group',
},
];
const displayOptions = {
show: {
resource: ['group'],
operation: ['get'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
@@ -0,0 +1,72 @@
import type { INodeProperties } from 'n8n-workflow';
import { updateDisplayOptions } from 'n8n-workflow';
import { userPoolResourceLocator } from '../common.description';
const properties: INodeProperties[] = [
{
...userPoolResourceLocator,
description: 'Select the user pool to use',
},
{
displayName: 'Return All',
name: 'returnAll',
default: false,
description: 'Whether to return all results or only up to a given limit',
type: 'boolean',
routing: {
operations: {
pagination: {
type: 'generic',
properties: {
continue: '={{ !!$response.body?.NextToken }}',
request: {
body: {
NextToken: '={{ $response.body?.NextToken }}',
},
},
},
},
},
},
},
{
displayName: 'Limit',
name: 'limit',
required: true,
type: 'number',
typeOptions: {
minValue: 1,
maxValue: 60,
},
default: 50,
description: 'Max number of results to return',
displayOptions: {
show: {
returnAll: [false],
},
},
routing: {
send: {
type: 'body',
property: 'Limit',
},
},
},
{
displayName: 'Include Users',
name: 'includeUsers',
type: 'boolean',
default: false,
description: 'Whether to include a list of users in the group',
},
];
const displayOptions = {
show: {
resource: ['group'],
operation: ['getAll'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
@@ -0,0 +1,107 @@
import type {
IDataObject,
IExecuteSingleFunctions,
IHttpRequestOptions,
INodeProperties,
} from 'n8n-workflow';
import { NodeApiError, updateDisplayOptions } from 'n8n-workflow';
import { validateArn } from '../../helpers/utils';
import { groupResourceLocator, userPoolResourceLocator } from '../common.description';
const properties: INodeProperties[] = [
{
...userPoolResourceLocator,
description: 'Select the user pool to use',
},
{
...groupResourceLocator,
description: 'Select the group you want to update',
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
placeholder: 'Add Option',
type: 'collection',
default: {},
routing: {
send: {
preSend: [
async function (
this: IExecuteSingleFunctions,
requestOptions: IHttpRequestOptions,
): Promise<IHttpRequestOptions> {
const additionalFields = this.getNodeParameter('additionalFields', {}) as IDataObject;
const arn = additionalFields.arn as string | undefined;
const description = additionalFields.description as string | undefined;
const precedence = additionalFields.precedence as number | undefined;
if (!description && !precedence && !arn) {
throw new NodeApiError(this.getNode(), {
message: 'At least one field must be provided for update.',
description: 'Please provide a value for Description, Precedence, or Role ARN.',
});
}
return requestOptions;
},
],
},
},
options: [
{
displayName: 'Description',
name: 'description',
default: '',
placeholder: 'e.g. Updated group description',
description: 'A new description for the group',
type: 'string',
routing: {
send: {
type: 'body',
property: 'Description',
},
},
},
{
displayName: 'Precedence',
name: 'precedence',
default: '',
placeholder: 'e.g. 10',
description:
'The new precedence value for the group. Lower values indicate higher priority.',
type: 'number',
routing: {
send: {
type: 'body',
property: 'Precedence',
},
},
validateType: 'number',
},
{
displayName: 'Role ARN',
name: 'arn',
default: '',
placeholder: 'e.g. arn:aws:iam::123456789012:role/GroupRole',
description:
'A new role Amazon Resource Name (ARN) for the group. Used for setting claims in tokens.',
type: 'string',
routing: {
send: {
type: 'body',
property: 'Arn',
preSend: [validateArn],
},
},
},
],
},
];
const displayOptions = {
show: {
resource: ['group'],
operation: ['update'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
@@ -0,0 +1,3 @@
export * as group from './group/Group.resource';
export * as user from './user/User.resource';
export * as userPool from './userPool/UserPool.resource';
@@ -0,0 +1,229 @@
import type { INodeProperties } from 'n8n-workflow';
import * as addToGroup from './addToGroup.operation';
import * as create from './create.operation';
import * as del from './delete.operation';
import * as get from './get.operation';
import * as getAll from './getAll.operation';
import * as removeFromGroup from './removeFromGroup.operation';
import * as update from './update.operation';
import { handleError } from '../../helpers/errorHandler';
import { preSendUserFields, simplifyUser } from '../../helpers/utils';
export const description: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
default: 'getAll',
displayOptions: {
show: {
resource: ['user'],
},
},
options: [
{
name: 'Add to Group',
value: 'addToGroup',
description: 'Add an existing user to a group',
action: 'Add user to group',
routing: {
send: {
preSend: [preSendUserFields],
},
request: {
method: 'POST',
headers: {
'X-Amz-Target': 'AWSCognitoIdentityProviderService.AdminAddUserToGroup',
},
ignoreHttpStatusErrors: true,
},
output: {
postReceive: [
handleError,
{
type: 'set',
properties: {
value: '={{ { "addedToGroup": true } }}',
},
},
],
},
},
},
{
name: 'Create',
value: 'create',
description: 'Create a new user',
action: 'Create user',
routing: {
send: {
preSend: [preSendUserFields],
},
request: {
method: 'POST',
headers: {
'X-Amz-Target': 'AWSCognitoIdentityProviderService.AdminCreateUser',
},
ignoreHttpStatusErrors: true,
},
output: {
postReceive: [
handleError,
{
type: 'rootProperty',
properties: {
property: 'User',
},
},
],
},
},
},
{
name: 'Delete',
value: 'delete',
description: 'Delete a user',
action: 'Delete user',
routing: {
send: {
preSend: [preSendUserFields],
},
request: {
method: 'POST',
headers: {
'X-Amz-Target': 'AWSCognitoIdentityProviderService.AdminDeleteUser',
},
ignoreHttpStatusErrors: true,
},
output: {
postReceive: [
handleError,
{
type: 'set',
properties: {
value: '={{ { "deleted": true } }}',
},
},
],
},
},
},
{
name: 'Get',
value: 'get',
description: 'Retrieve information of an existing user',
action: 'Get user',
routing: {
send: {
preSend: [preSendUserFields],
},
request: {
method: 'POST',
headers: {
'X-Amz-Target': 'AWSCognitoIdentityProviderService.AdminGetUser',
},
ignoreHttpStatusErrors: true,
},
output: {
postReceive: [handleError, simplifyUser],
},
},
},
{
name: 'Get Many',
value: 'getAll',
description: 'Retrieve a list of users',
routing: {
request: {
method: 'POST',
headers: {
'X-Amz-Target': 'AWSCognitoIdentityProviderService.ListUsers',
},
ignoreHttpStatusErrors: true,
},
output: {
postReceive: [
handleError,
simplifyUser,
{
type: 'rootProperty',
properties: {
property: 'Users',
},
},
],
},
},
action: 'Get many users',
},
{
name: 'Remove From Group',
value: 'removeFromGroup',
description: 'Remove a user from a group',
action: 'Remove user from group',
routing: {
send: {
preSend: [preSendUserFields],
},
request: {
method: 'POST',
headers: {
'X-Amz-Target': 'AWSCognitoIdentityProviderService.AdminRemoveUserFromGroup',
},
ignoreHttpStatusErrors: true,
},
output: {
postReceive: [
handleError,
{
type: 'set',
properties: {
value: '={{ { "removedFromGroup": true } }}',
},
},
],
},
},
},
{
name: 'Update',
value: 'update',
description: 'Update an existing user',
action: 'Update user',
routing: {
send: {
preSend: [preSendUserFields],
},
request: {
method: 'POST',
headers: {
'X-Amz-Target': 'AWSCognitoIdentityProviderService.AdminUpdateUserAttributes',
},
ignoreHttpStatusErrors: true,
},
output: {
postReceive: [
handleError,
{
type: 'set',
properties: {
value: '={{ { "updated": true } }}',
},
},
],
},
},
},
],
},
...create.description,
...del.description,
...get.description,
...getAll.description,
...update.description,
...addToGroup.description,
...removeFromGroup.description,
];
@@ -0,0 +1,32 @@
import type { INodeProperties } from 'n8n-workflow';
import { updateDisplayOptions } from 'n8n-workflow';
import {
groupResourceLocator,
userPoolResourceLocator,
userResourceLocator,
} from '../common.description';
const properties: INodeProperties[] = [
{
...userPoolResourceLocator,
description: 'Select the user pool to use',
},
{
...userResourceLocator,
description: 'Select the user you want to add to the group',
},
{
...groupResourceLocator,
description: 'Select the group you want to add the user to',
},
];
const displayOptions = {
show: {
resource: ['user'],
operation: ['addToGroup'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
@@ -0,0 +1,280 @@
import type { INodeProperties } from 'n8n-workflow';
import { updateDisplayOptions } from 'n8n-workflow';
import { preSendAttributes, preSendDesiredDeliveryMediums } from '../../helpers/utils';
import { userPoolResourceLocator } from '../common.description';
const properties: INodeProperties[] = [
{
...userPoolResourceLocator,
description: 'Select the user pool to retrieve',
},
{
displayName: 'User Name',
name: 'newUserName',
default: '',
description:
'Depending on the user pool settings, this parameter requires the username, the email, or the phone number. No whitespace is allowed.',
placeholder: 'e.g. JohnSmith',
required: true,
routing: {
send: {
property: 'Username',
type: 'body',
},
},
type: 'string',
validateType: 'string',
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
options: [
{
displayName: 'Message Action',
name: 'messageAction',
default: 'RESEND',
type: 'options',
options: [
{
name: 'Resend',
value: 'RESEND',
description:
"Resend the invitation message to a user that already exists and reset the expiration limit on the user's account",
},
{
name: 'Suppress',
value: 'SUPPRESS',
description: 'Suppress sending the message',
},
],
routing: {
send: {
property: 'MessageAction',
type: 'body',
},
},
},
{
displayName: 'Force Alias Creation',
name: 'forceAliasCreation',
type: 'boolean',
validateType: 'boolean',
default: false,
description:
'Whether this parameter is used only if the phone_number_verified or email_verified attribute is set to true. Otherwise, it is ignored. If set to true, and the phone number or email address specified in the UserAttributes parameter already exists as an alias with a different user, the alias will be migrated. If set to false, an AliasExistsException error is thrown if the alias already exists.',
routing: {
send: {
type: 'body',
property: 'ForceAliasCreation',
},
},
},
{
displayName: 'User Attributes',
name: 'userAttributes',
type: 'fixedCollection',
placeholder: 'Add Attribute',
default: {
attributes: [],
},
required: true,
description: 'Attributes to update for the user',
typeOptions: {
multipleValues: true,
},
routing: {
send: {
preSend: [preSendAttributes],
},
},
options: [
{
displayName: 'Attributes',
name: 'attributes',
values: [
{
displayName: 'Attribute Type',
name: 'attributeType',
type: 'options',
default: 'standard',
options: [
{
name: 'Standard Attribute',
value: 'standard',
},
{
name: 'Custom Attribute',
value: 'custom',
},
],
},
{
displayName: 'Standard Attribute',
name: 'standardName',
type: 'options',
default: 'address',
options: [
{
name: 'Address',
value: 'address',
},
{
name: 'Birthdate',
value: 'birthdate',
},
{
name: 'Email',
value: 'email',
},
{
name: 'Email Verified',
value: 'email_verified',
},
{
name: 'Family Name',
value: 'family_name',
},
{
name: 'Gender',
value: 'gender',
},
{
name: 'Given Name',
value: 'given_name',
},
{
name: 'Locale',
value: 'locale',
},
{
name: 'Middle Name',
value: 'middle_name',
},
{
name: 'Name',
value: 'name',
},
{
name: 'Nickname',
value: 'nickname',
},
{
name: 'Phone Number',
value: 'phone_number',
},
{
name: 'Phone Number Verified',
value: 'phone_number_verified',
},
{
name: 'Preferred Username',
value: 'preferred_username',
},
{
name: 'Profile Picture',
value: 'profilepicture',
},
{
name: 'Updated At',
value: 'updated_at',
},
{
name: 'User Sub',
value: 'sub',
},
{
name: 'Website',
value: 'website',
},
{
name: 'Zone Info',
value: 'zoneinfo',
},
],
displayOptions: {
show: {
attributeType: ['standard'],
},
},
},
{
displayName: 'Custom Attribute Name',
name: 'customName',
type: 'string',
default: '',
placeholder: 'custom:myAttribute',
description: 'The name of the custom attribute (must start with "custom:")',
displayOptions: {
show: {
attributeType: ['custom'],
},
},
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
description: 'The value of the attribute',
},
],
},
],
},
{
displayName: 'Desired Delivery Mediums',
name: 'desiredDeliveryMediums',
default: ['SMS'],
description: 'Specify how to send the welcome message',
type: 'multiOptions',
options: [
{
name: 'SMS',
value: 'SMS',
},
{
name: 'Email',
value: 'EMAIL',
},
],
routing: {
send: {
preSend: [preSendDesiredDeliveryMediums],
property: 'DesiredDeliveryMediums',
type: 'body',
},
},
},
{
displayName: 'Temporary Password',
name: 'temporaryPasswordOptions',
type: 'string',
typeOptions: {
password: true,
},
default: '',
description:
"The user's temporary password that will be valid only once. If not set, Amazon Cognito will automatically generate one for you.",
routing: {
send: {
property: 'TemporaryPassword',
type: 'body',
},
},
},
],
},
];
const displayOptions = {
show: {
resource: ['user'],
operation: ['create'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
@@ -0,0 +1,24 @@
import type { INodeProperties } from 'n8n-workflow';
import { updateDisplayOptions } from 'n8n-workflow';
import { userPoolResourceLocator, userResourceLocator } from '../common.description';
const properties: INodeProperties[] = [
{
...userPoolResourceLocator,
description: 'Select the user pool to use',
},
{
...userResourceLocator,
description: 'Select the user you want to delete',
},
];
const displayOptions = {
show: {
resource: ['user'],
operation: ['delete'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
@@ -0,0 +1,31 @@
import type { INodeProperties } from 'n8n-workflow';
import { updateDisplayOptions } from 'n8n-workflow';
import { userPoolResourceLocator, userResourceLocator } from '../common.description';
const properties: INodeProperties[] = [
{
...userPoolResourceLocator,
description: 'Select the user pool to use',
},
{
...userResourceLocator,
description: 'Select the user you want to retrieve',
},
{
displayName: 'Simplify',
name: 'simple',
type: 'boolean',
default: true,
description: 'Whether to return a simplified version of the response instead of the raw data',
},
];
const displayOptions = {
show: {
resource: ['user'],
operation: ['get'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
@@ -0,0 +1,174 @@
import type {
IDataObject,
IExecuteSingleFunctions,
IHttpRequestOptions,
INodeProperties,
} from 'n8n-workflow';
import { jsonParse, updateDisplayOptions } from 'n8n-workflow';
import type { Filters } from '../../helpers/interfaces';
import { userPoolResourceLocator } from '../common.description';
const properties: INodeProperties[] = [
{
...userPoolResourceLocator,
description: 'Select the user pool to use',
},
{
displayName: 'Return All',
name: 'returnAll',
default: false,
description: 'Whether to return all results or only up to a given limit',
type: 'boolean',
routing: {
operations: {
pagination: {
type: 'generic',
properties: {
continue: '={{ !!$response.body?.PaginationToken }}',
request: {
body: {
PaginationToken: '={{ $response.body?.PaginationToken }}',
},
},
},
},
},
},
},
{
displayName: 'Limit',
name: 'limit',
required: true,
type: 'number',
typeOptions: {
minValue: 1,
maxValue: 60,
},
default: 50,
description: 'Max number of results to return',
displayOptions: {
show: {
returnAll: [false],
},
},
routing: {
send: {
type: 'body',
property: 'Limit',
},
},
},
{
displayName: 'Simplify',
name: 'simple',
type: 'boolean',
default: true,
description: 'Whether to return a simplified version of the response instead of the raw data',
},
{
displayName: 'Filters',
name: 'filters',
type: 'fixedCollection',
placeholder: 'Add Filter',
default: {},
routing: {
send: {
preSend: [
async function (
this: IExecuteSingleFunctions,
requestOptions: IHttpRequestOptions,
): Promise<IHttpRequestOptions> {
const filters = this.getNodeParameter('filters', {}) as Filters;
const filter = filters.filter;
if (!filter?.value) return requestOptions;
const { attribute: filterAttribute, value: filterValue } = filter;
const body = jsonParse<IDataObject>(String(requestOptions.body), {
acceptJSObject: true,
errorMessage: 'Invalid request body. Request body must be valid JSON.',
});
const filterString = filterAttribute ? `"${filterAttribute}"^="${filterValue}"` : '';
return {
...requestOptions,
body: JSON.stringify({ ...body, Filter: filterString }),
};
},
],
},
},
options: [
{
displayName: 'Filter',
name: 'filter',
values: [
{
displayName: 'Attribute',
name: 'attribute',
type: 'options',
default: 'email',
description: 'The attribute to search for',
options: [
{
name: 'Cognito User Status',
value: 'cognito:user_status',
},
{
name: 'Email',
value: 'email',
},
{
name: 'Family Name',
value: 'family_name',
},
{
name: 'Given Name',
value: 'given_name',
},
{
name: 'Name',
value: 'name',
},
{
name: 'Phone Number',
value: 'phone_number',
},
{
name: 'Preferred Username',
value: 'preferred_username',
},
{
name: 'Status (Enabled)',
value: 'status',
},
{
name: 'Sub',
value: 'sub',
},
{
name: 'Username',
value: 'username',
},
],
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
description: 'The value of the attribute to search for',
},
],
},
],
},
];
const displayOptions = {
show: {
resource: ['user'],
operation: ['getAll'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
@@ -0,0 +1,43 @@
import type { INodeProperties } from 'n8n-workflow';
import { updateDisplayOptions } from 'n8n-workflow';
import {
groupResourceLocator,
userPoolResourceLocator,
userResourceLocator,
} from '../common.description';
const properties: INodeProperties[] = [
{
...userPoolResourceLocator,
description: 'Select the user pool to use',
},
{
...userResourceLocator,
description: 'Select the user you want to remove from the group',
},
{
...groupResourceLocator,
description: 'Select the group you want to remove the user from',
modes: groupResourceLocator.modes?.map((mode) =>
mode.name === 'list'
? {
...mode,
typeOptions: {
...mode.typeOptions,
searchListMethod: 'searchGroupsForUser',
},
}
: mode,
),
},
];
const displayOptions = {
show: {
resource: ['user'],
operation: ['removeFromGroup'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
@@ -0,0 +1,166 @@
import type { INodeProperties } from 'n8n-workflow';
import { updateDisplayOptions } from 'n8n-workflow';
import { preSendAttributes } from '../../helpers/utils';
import { userPoolResourceLocator, userResourceLocator } from '../common.description';
const properties: INodeProperties[] = [
{
...userPoolResourceLocator,
description: 'Select the user pool to use',
},
userResourceLocator,
{
displayName: 'User Attributes',
name: 'userAttributes',
type: 'fixedCollection',
placeholder: 'Add Attribute',
default: {
attributes: [],
},
required: true,
description: 'Attributes to update for the user',
typeOptions: {
multipleValues: true,
},
routing: {
send: {
preSend: [preSendAttributes],
},
},
options: [
{
displayName: 'Attributes',
name: 'attributes',
values: [
{
displayName: 'Attribute Type',
name: 'attributeType',
type: 'options',
default: 'standard',
options: [
{
name: 'Standard Attribute',
value: 'standard',
},
{
name: 'Custom Attribute',
value: 'custom',
},
],
},
{
displayName: 'Standard Attribute',
name: 'standardName',
type: 'options',
default: 'address',
options: [
{
name: 'Address',
value: 'address',
},
{
name: 'Birthdate',
value: 'birthdate',
},
{
name: 'Email',
value: 'email',
},
{
name: 'Family Name',
value: 'family_name',
},
{
name: 'Gender',
value: 'gender',
},
{
name: 'Given Name',
value: 'given_name',
},
{
name: 'Locale',
value: 'locale',
},
{
name: 'Middle Name',
value: 'middle_name',
},
{
name: 'Name',
value: 'name',
},
{
name: 'Nickname',
value: 'nickname',
},
{
name: 'Phone Number',
value: 'phone_number',
},
{
name: 'Preferred Username',
value: 'preferred_username',
},
{
name: 'Profile Picture',
value: 'profilepicture',
},
{
name: 'Updated At',
value: 'updated_at',
},
{
name: 'User Sub',
value: 'sub',
},
{
name: 'Website',
value: 'website',
},
{
name: 'Zone Info',
value: 'zoneinfo',
},
],
displayOptions: {
show: {
attributeType: ['standard'],
},
},
},
{
displayName: 'Custom Attribute Name',
name: 'customName',
type: 'string',
default: '',
placeholder: 'custom:myAttribute',
description: 'The name of the custom attribute (must start with "custom:")',
displayOptions: {
show: {
attributeType: ['custom'],
},
},
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
description: 'The value of the attribute',
},
],
},
],
},
];
const displayOptions = {
show: {
resource: ['user'],
operation: ['update'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
@@ -0,0 +1,47 @@
import type { INodeProperties } from 'n8n-workflow';
import * as get from './get.operation';
import { simplifyUserPool } from '../../helpers/utils';
export const description: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['userPool'],
},
},
options: [
{
name: 'Get',
value: 'get',
action: 'Get user pool',
routing: {
request: {
method: 'POST',
headers: {
'X-Amz-Target': 'AWSCognitoIdentityProviderService.DescribeUserPool',
},
},
output: {
postReceive: [
simplifyUserPool,
{
type: 'rootProperty',
properties: {
property: 'UserPool',
},
},
],
},
},
},
],
default: 'get',
},
...get.description,
];
@@ -0,0 +1,27 @@
import type { INodeProperties } from 'n8n-workflow';
import { updateDisplayOptions } from 'n8n-workflow';
import { userPoolResourceLocator } from '../common.description';
const properties: INodeProperties[] = [
{
...userPoolResourceLocator,
description: 'Select the user pool to retrieve',
},
{
displayName: 'Simplify',
name: 'simple',
type: 'boolean',
default: true,
description: 'Whether to return a simplified version of the response instead of the raw data',
},
];
const displayOptions = {
show: {
resource: ['userPool'],
operation: ['get'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
@@ -0,0 +1,64 @@
export const HeaderConstants = {
AUTHORIZATION: 'authorization',
X_MS_CONTINUATION: 'x-ms-continuation',
X_MS_COSMOS_OFFER_AUTOPILOT_SETTING: 'x-ms-cosmos-offer-autopilot-setting',
X_MS_DOCUMENTDB_IS_UPSERT: 'x-ms-documentdb-is-upsert',
X_MS_DOCUMENTDB_PARTITIONKEY: 'x-ms-documentdb-partitionkey',
X_MS_MAX_ITEM_COUNT: 'x-ms-max-item-count',
X_MS_OFFER_THROUGHPUT: 'x-ms-offer-throughput',
};
export const ERROR_MESSAGES = {
ResourceNotFound: {
Group: {
delete: {
message: 'The group you are trying to delete could not be found.',
description: 'Adjust the "Group" parameter setting to delete the group correctly.',
},
get: {
message: 'The group you are trying to retrieve could not be found.',
description: 'Adjust the "Group" parameter setting to retrieve the group correctly.',
},
update: {
message: 'The group you are trying to update could not be found.',
description: 'Adjust the "Group" parameter setting to update the group correctly.',
},
},
User: {
delete: {
message: 'The user are trying to retrieve could not be found.',
description: 'Adjust the "User" parameter setting to delete the user correctly.',
},
get: {
message: 'The user you are trying to retrieve could not be found.',
description: 'Adjust the "User" parameter setting to retrieve the user correctly.',
},
update: {
message: 'The user you are trying to update could not be found.',
description: 'Adjust the "User" parameter setting to update the user correctly.',
},
},
},
EntityAlreadyExists: {
Group: {
message: 'The group you are trying to create already exists.',
description: 'Adjust the "Group Name" parameter setting to create the group correctly.',
},
User: {
message: 'The user you are trying to create already exists.',
description: 'Adjust the "User Name" parameter setting to create the user correctly.',
},
},
UserGroup: {
add: {
message: 'The user/group you are trying to add could not be found.',
description:
'Adjust the "User" and "Group" parameters to add the user to the group correctly.',
},
remove: {
message: 'The user/group you are trying to remove could not be found.',
description:
'Adjust the "User" and "Group" parameters to remove the user from the group correctly.',
},
},
};
@@ -0,0 +1,117 @@
import type {
JsonObject,
IExecuteSingleFunctions,
IN8nHttpFullResponse,
INodeExecutionData,
} from 'n8n-workflow';
import { NodeApiError } from 'n8n-workflow';
import { ERROR_MESSAGES } from './constants';
import type { AwsError, ErrorMessage } from './interfaces';
function mapErrorToResponse(
errorType: string,
resource: string,
operation: string,
inputValue?: string,
): ErrorMessage | undefined {
const op = operation as keyof typeof ERROR_MESSAGES.ResourceNotFound.User;
const nameLabel = resource.charAt(0).toUpperCase() + resource.slice(1);
const valuePart = inputValue ? ` "${inputValue}"` : '';
const notFoundMessage = (base: ErrorMessage, suffix: string): ErrorMessage => ({
...base,
message: `${nameLabel}${valuePart} ${suffix}`,
});
const isNotFound = [
'UserNotFoundException',
'ResourceNotFoundException',
'NoSuchEntity',
].includes(errorType);
const isExists = [
'UsernameExistsException',
'EntityAlreadyExists',
'GroupExistsException',
].includes(errorType);
if (isNotFound) {
if (resource === 'user') {
if (operation === 'addToGroup') {
return notFoundMessage(ERROR_MESSAGES.UserGroup.add, 'not found while adding to group.');
}
if (operation === 'removeFromGroup') {
return notFoundMessage(
ERROR_MESSAGES.UserGroup.remove,
'not found while removing from group.',
);
}
return notFoundMessage(ERROR_MESSAGES.ResourceNotFound.User[op], 'not found.');
}
if (resource === 'group') {
return notFoundMessage(ERROR_MESSAGES.ResourceNotFound.Group[op], 'not found.');
}
}
if (isExists) {
const existsMessage = `${nameLabel}${valuePart} already exists.`;
if (resource === 'user') {
return { ...ERROR_MESSAGES.EntityAlreadyExists.User, message: existsMessage };
}
if (resource === 'group') {
return { ...ERROR_MESSAGES.EntityAlreadyExists.Group, message: existsMessage };
}
}
return undefined;
}
export async function handleError(
this: IExecuteSingleFunctions,
data: INodeExecutionData[],
response: IN8nHttpFullResponse,
): Promise<INodeExecutionData[]> {
const statusCode = String(response.statusCode);
if (!statusCode.startsWith('4') && !statusCode.startsWith('5')) {
return data;
}
const resource = this.getNodeParameter('resource') as string;
const operation = this.getNodeParameter('operation') as string;
let inputValue: string | undefined;
if (operation === 'create') {
if (resource === 'user') {
inputValue = this.getNodeParameter('newUserName', '') as string;
} else if (resource === 'group') {
inputValue = this.getNodeParameter('newGroupName', '') as string;
}
} else {
inputValue = this.getNodeParameter(resource, '', { extractValue: true }) as string;
}
const responseBody = response.body as AwsError;
const errorType = (responseBody.__type ?? response.headers?.['x-amzn-errortype']) as string;
const errorMessage = (responseBody.message ??
response.headers?.['x-amzn-errormessage']) as string;
if (!errorType) {
throw new NodeApiError(this.getNode(), response as unknown as JsonObject);
}
const specificError = mapErrorToResponse(errorType, resource, operation, inputValue);
throw new NodeApiError(
this.getNode(),
response as unknown as JsonObject,
specificError ?? {
message: errorType,
description: errorMessage,
},
);
}
@@ -0,0 +1,73 @@
import type { IDataObject } from 'n8n-workflow';
export interface IUserAttribute {
Name: string;
Value: string;
}
export interface IUser {
Username: string;
Enabled: boolean;
UserCreateDate: string;
UserLastModifiedDate: string;
UserStatus: string;
Attributes?: IUserAttribute[];
}
export interface IGroup {
GroupName: string;
}
export interface IListUsersResponse {
Users: IUser[];
NextToken?: string;
}
export interface IListGroupsResponse {
Groups: IGroup[];
NextToken?: string;
}
export interface IGroupWithUserResponse extends IGroup {
Users: IUser[];
}
export interface IUserAttributeInput {
attributeType: string;
standardName: string;
customName: string;
value: string;
}
export interface IUserPool {
Id: string;
Name: string;
UsernameAttributes?: string[];
AccountRecoverySetting?: IDataObject;
AdminCreateUserConfig?: IDataObject;
EmailConfiguration?: IDataObject;
LambdaConfig?: IDataObject;
Policies?: IDataObject;
SchemaAttributes?: IDataObject;
UserAttributeUpdateSettings?: IDataObject;
UserPoolTags?: IDataObject;
UserPoolTier?: string;
VerificationMessageTemplate?: IDataObject;
}
export interface Filters {
filter?: {
attribute?: string;
value?: string;
};
}
export interface AwsError {
__type?: string;
message?: string;
}
export interface ErrorMessage {
message: string;
description: string;
}
@@ -0,0 +1,424 @@
import type {
IHttpRequestOptions,
ILoadOptionsFunctions,
IDataObject,
IExecuteSingleFunctions,
IN8nHttpFullResponse,
INodeExecutionData,
} from 'n8n-workflow';
import { jsonParse, NodeApiError, NodeOperationError } from 'n8n-workflow';
import type {
IGroup,
IGroupWithUserResponse,
IListGroupsResponse,
IUser,
IUserAttribute,
IUserAttributeInput,
IUserPool,
} from './interfaces';
import { awsApiRequest, awsApiRequestAllItems } from '../transport';
const validateEmail = (email: string): boolean => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
const validatePhoneNumber = (phone: string): boolean => /^\+[0-9]\d{1,14}$/.test(phone);
export async function preSendStringifyBody(
this: IExecuteSingleFunctions,
requestOptions: IHttpRequestOptions,
): Promise<IHttpRequestOptions> {
if (requestOptions.body) {
requestOptions.body = JSON.stringify(requestOptions.body);
}
return requestOptions;
}
export async function getUserPool(
this: IExecuteSingleFunctions | ILoadOptionsFunctions,
userPoolId: string,
): Promise<IUserPool> {
if (!userPoolId) {
throw new NodeOperationError(this.getNode(), 'User Pool ID is required');
}
const response = (await awsApiRequest.call(
this,
'POST',
'DescribeUserPool',
JSON.stringify({ UserPoolId: userPoolId }),
)) as { UserPool: IUserPool };
if (!response?.UserPool) {
throw new NodeOperationError(this.getNode(), 'User Pool not found in response');
}
return response.UserPool;
}
export async function getUsersInGroup(
this: IExecuteSingleFunctions | ILoadOptionsFunctions,
groupName: string,
userPoolId: string,
): Promise<IUser[]> {
if (!userPoolId) {
throw new NodeOperationError(this.getNode(), 'User Pool ID is required');
}
const requestBody: IDataObject = {
UserPoolId: userPoolId,
GroupName: groupName,
};
const allUsers = (await awsApiRequestAllItems.call(
this,
'POST',
'ListUsersInGroup',
requestBody,
'Users',
)) as unknown as IUser[];
return allUsers;
}
export async function getUserNameFromExistingUsers(
this: IExecuteSingleFunctions | ILoadOptionsFunctions,
userName: string,
userPoolId: string,
isEmailOrPhone: boolean,
): Promise<string | undefined> {
if (isEmailOrPhone) {
return userName;
}
const usersResponse = (await awsApiRequest.call(
this,
'POST',
'ListUsers',
JSON.stringify({
UserPoolId: userPoolId,
Filter: `sub = "${userName}"`,
}),
)) as { Users: IUser[] };
const username =
usersResponse.Users && usersResponse.Users.length > 0
? usersResponse.Users[0].Username
: undefined;
return username;
}
export async function preSendUserFields(
this: IExecuteSingleFunctions,
requestOptions: IHttpRequestOptions,
): Promise<IHttpRequestOptions> {
const operation = this.getNodeParameter('operation') as string;
const userPoolId = this.getNodeParameter('userPool', undefined, {
extractValue: true,
}) as string;
const userPool = await getUserPool.call(this, userPoolId);
const usernameAttributes = userPool.UsernameAttributes ?? [];
const isEmailAuth = usernameAttributes.includes('email');
const isPhoneAuth = usernameAttributes.includes('phone_number');
const isEmailOrPhone = isEmailAuth || isPhoneAuth;
const getValidatedNewUserName = (): string => {
const newUsername = this.getNodeParameter('newUserName') as string;
if (isEmailAuth && !validateEmail(newUsername)) {
throw new NodeApiError(this.getNode(), {
message: 'Invalid email format',
description: 'Please provide a valid email (e.g., name@gmail.com)',
});
}
if (isPhoneAuth && !validatePhoneNumber(newUsername)) {
throw new NodeApiError(this.getNode(), {
message: 'Invalid phone number format',
description: 'Please provide a valid phone number (e.g., +14155552671)',
});
}
return newUsername;
};
const finalUserName =
operation === 'create'
? getValidatedNewUserName()
: await getUserNameFromExistingUsers.call(
this,
this.getNodeParameter('user', undefined, { extractValue: true }) as string,
userPoolId,
isEmailOrPhone,
);
const body = jsonParse<IDataObject>(String(requestOptions.body), {
acceptJSObject: true,
errorMessage: 'Invalid request body. Request body must be valid JSON.',
});
return {
...requestOptions,
body: JSON.stringify({
...body,
...(finalUserName ? { Username: finalUserName } : {}),
}),
};
}
export async function processGroup(
this: IExecuteSingleFunctions,
items: INodeExecutionData[],
response: IN8nHttpFullResponse,
): Promise<INodeExecutionData[]> {
const userPoolId = this.getNodeParameter('userPool', undefined, {
extractValue: true,
}) as string;
const includeUsers = this.getNodeParameter('includeUsers') as boolean;
const body = response.body as IDataObject;
if (body.Group) {
const group = body.Group as IGroup;
if (!includeUsers) {
return this.helpers.returnJsonArray({ ...group });
}
const users = await getUsersInGroup.call(this, group.GroupName, userPoolId);
return this.helpers.returnJsonArray({ ...group, Users: users });
}
const groups = (response.body as IListGroupsResponse).Groups ?? [];
if (!includeUsers) {
return items;
}
const processedGroups: IGroupWithUserResponse[] = [];
for (const group of groups) {
const users = await getUsersInGroup.call(this, group.GroupName, userPoolId);
processedGroups.push({
...group,
Users: users,
});
}
return items.map((item) => ({ json: { ...item.json, Groups: processedGroups } }));
}
export async function validateArn(
this: IExecuteSingleFunctions,
requestOptions: IHttpRequestOptions,
): Promise<IHttpRequestOptions> {
const arn = this.getNodeParameter('additionalFields.arn', '') as string;
const arnRegex =
/^arn:[-.\w+=/,@]+:[-.\w+=/,@]+:([-.\w+=/,@]*)?:[0-9]+:[-.\w+=/,@]+(:[-.\w+=/,@]+)?(:[-.\w+=/,@]+)?$/;
if (!arnRegex.test(arn)) {
throw new NodeApiError(this.getNode(), {
message: 'Invalid ARN format',
description:
'Please provide a valid AWS ARN (e.g., arn:aws:iam::123456789012:role/GroupRole).',
});
}
return requestOptions;
}
export async function simplifyUserPool(
this: IExecuteSingleFunctions,
items: INodeExecutionData[],
_response: IN8nHttpFullResponse,
): Promise<INodeExecutionData[]> {
const simple = this.getNodeParameter('simple') as boolean;
if (!simple) {
return items;
}
return items
.map((item) => {
const data = item.json?.UserPool as IUserPool;
if (!data) {
return;
}
const {
AccountRecoverySetting,
AdminCreateUserConfig,
EmailConfiguration,
LambdaConfig,
Policies,
SchemaAttributes,
UserAttributeUpdateSettings,
UserPoolTags,
UserPoolTier,
VerificationMessageTemplate,
...selectedData
} = data;
return { json: { UserPool: { ...selectedData } } };
})
.filter(Boolean) as INodeExecutionData[];
}
export async function simplifyUser(
this: IExecuteSingleFunctions,
items: INodeExecutionData[],
_response: IN8nHttpFullResponse,
): Promise<INodeExecutionData[]> {
const simple = this.getNodeParameter('simple') as boolean;
if (!simple) {
return items;
}
return items
.map((item) => {
const data = item.json;
if (!data) {
return;
}
if (Array.isArray(data.Users)) {
const users = data.Users as IUser[];
const simplifiedUsers = users.map((user) => {
const attributesArray = user.Attributes ?? [];
const userAttributes = Object.fromEntries(
attributesArray
.filter(({ Name }) => Name?.trim())
.map(({ Name, Value }) => [Name, Value ?? '']),
);
const { Attributes, ...rest } = user;
return { ...rest, ...userAttributes };
});
return { json: { ...data, Users: simplifiedUsers } };
}
if (Array.isArray(data.UserAttributes)) {
const attributesArray = data.UserAttributes as IUserAttribute[];
const userAttributes = Object.fromEntries(
attributesArray
.filter(({ Name }) => Name?.trim())
.map(({ Name, Value }) => [Name, Value ?? '']),
);
const { UserAttributes, ...rest } = data;
return { json: { ...rest, ...userAttributes } };
}
return item;
})
.filter(Boolean) as INodeExecutionData[];
}
export async function preSendAttributes(
this: IExecuteSingleFunctions,
requestOptions: IHttpRequestOptions,
): Promise<IHttpRequestOptions> {
const operation = this.getNodeParameter('operation', 0) as string;
const parameterName =
operation === 'create'
? 'additionalFields.userAttributes.attributes'
: 'userAttributes.attributes';
const attributes = this.getNodeParameter(parameterName, []) as IUserAttributeInput[];
if (operation === 'update' && (!attributes || attributes.length === 0)) {
throw new NodeOperationError(this.getNode(), 'No user attributes provided', {
description: 'At least one user attribute must be provided for the update operation.',
});
}
if (operation === 'create') {
const hasEmail = attributes.some((a) => a.standardName === 'email');
const hasEmailVerified = attributes.some(
(a) => a.standardName === 'email_verified' && a.value === 'true',
);
if (hasEmailVerified && !hasEmail) {
throw new NodeOperationError(this.getNode(), 'Missing required "email" attribute', {
description:
'"email_verified" is set to true, but the corresponding "email" attribute is not provided.',
});
}
const hasPhone = attributes.some((a) => a.standardName === 'phone_number');
const hasPhoneVerified = attributes.some(
(a) => a.standardName === 'phone_number_verified' && a.value === 'true',
);
if (hasPhoneVerified && !hasPhone) {
throw new NodeOperationError(this.getNode(), 'Missing required "phone_number" attribute', {
description:
'"phone_number_verified" is set to true, but the corresponding "phone_number" attribute is not provided.',
});
}
}
const body = jsonParse<IDataObject>(String(requestOptions.body), {
acceptJSObject: true,
errorMessage: 'Invalid request body. Request body must be valid JSON.',
});
body.UserAttributes = attributes.map(({ attributeType, standardName, customName, value }) => {
if (!value || !attributeType || !(standardName ?? customName)) {
throw new NodeOperationError(this.getNode(), 'Invalid User Attribute', {
description: 'Each attribute must have a valid name and value.',
});
}
const attributeName =
attributeType === 'standard'
? standardName
: `custom:${customName?.startsWith('custom:') ? customName : customName}`;
return { Name: attributeName, Value: value };
});
requestOptions.body = JSON.stringify(body);
return requestOptions;
}
export async function preSendDesiredDeliveryMediums(
this: IExecuteSingleFunctions,
requestOptions: IHttpRequestOptions,
): Promise<IHttpRequestOptions> {
const desiredDeliveryMediums = this.getNodeParameter(
'additionalFields.desiredDeliveryMediums',
[],
) as string[];
const attributes = this.getNodeParameter(
'additionalFields.userAttributes.attributes',
[],
) as IUserAttributeInput[];
const hasEmail = attributes.some((attr) => attr.standardName === 'email' && !!attr.value?.trim());
const hasPhone = attributes.some(
(attr) => attr.standardName === 'phone_number' && !!attr.value?.trim(),
);
if (desiredDeliveryMediums.includes('EMAIL') && !hasEmail) {
throw new NodeOperationError(this.getNode(), 'Missing required "email" attribute', {
description: 'Email is selected as a delivery medium but no email attribute is provided.',
});
}
if (desiredDeliveryMediums.includes('SMS') && !hasPhone) {
throw new NodeOperationError(this.getNode(), 'Missing required "phone_number" attribute', {
description:
'SMS is selected as a delivery medium but no phone_number attribute is provided.',
});
}
return requestOptions;
}
@@ -0,0 +1 @@
export * as listSearch from './listSearch';
@@ -0,0 +1,190 @@
import type {
IDataObject,
IExecuteSingleFunctions,
ILoadOptionsFunctions,
INodeListSearchItems,
INodeListSearchResult,
} from 'n8n-workflow';
import { NodeOperationError } from 'n8n-workflow';
import type { IGroup, IUser, IUserAttribute, IUserPool } from '../helpers/interfaces';
import { getUserNameFromExistingUsers, getUserPool } from '../helpers/utils';
import { awsApiRequest, awsApiRequestAllItems } from '../transport';
function formatResults(items: IDataObject[], filter?: string): INodeListSearchItems[] {
return items
.map(({ id, name }) => ({
name: String(name).replace(/ /g, ''),
value: String(id),
}))
.filter(({ name }) => !filter || name.toLowerCase().includes(filter.toLowerCase()))
.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()));
}
export async function searchGroups(
this: ILoadOptionsFunctions,
filter?: string,
paginationToken?: string,
): Promise<INodeListSearchResult> {
const userPoolId = this.getNodeParameter('userPool', undefined, {
extractValue: true,
}) as string;
if (!userPoolId) {
throw new NodeOperationError(this.getNode(), 'User Pool ID is required to search groups');
}
const responseData = (await awsApiRequest.call(
this,
'POST',
'ListGroups',
JSON.stringify({ UserPoolId: userPoolId, Limit: 50, NextToken: paginationToken }),
)) as IDataObject;
const groups = responseData.Groups as IDataObject[];
const groupsMapped = groups.map(({ GroupName }) => ({
id: GroupName,
name: GroupName,
}));
const formattedResults = formatResults(groupsMapped, filter);
return { results: formattedResults, paginationToken: responseData.NextToken };
}
export async function searchGroupsForUser(
this: ILoadOptionsFunctions,
filter?: string,
): Promise<INodeListSearchResult> {
const userPoolId = this.getNodeParameter('userPool', undefined, {
extractValue: true,
}) as string;
const inputUser = this.getNodeParameter('user', undefined, {
extractValue: true,
}) as string;
if (!userPoolId || !inputUser) {
return { results: [] };
}
const userPool = await getUserPool.call(this, userPoolId);
const usernameAttributes = userPool.UsernameAttributes ?? [];
const isEmailAuth = usernameAttributes.includes('email');
const isPhoneAuth = usernameAttributes.includes('phone_number');
const isEmailOrPhone = isEmailAuth || isPhoneAuth;
const userName = await getUserNameFromExistingUsers.call(
this,
inputUser,
userPoolId,
isEmailOrPhone,
);
if (!userName) {
return { results: [] };
}
const groups = (await awsApiRequestAllItems.call(
this,
'POST',
'AdminListGroupsForUser',
{
Username: userName,
UserPoolId: userPoolId,
},
'Groups',
)) as unknown as IGroup[];
const resultGroups = groups
.filter((group) => !filter || group.GroupName.toLowerCase().includes(filter.toLowerCase()))
.map((group) => ({
name: group.GroupName,
value: group.GroupName,
}))
.sort((a, b) => a.name.localeCompare(b.name));
return { results: resultGroups };
}
export async function searchUsers(
this: IExecuteSingleFunctions | ILoadOptionsFunctions,
filter?: string,
paginationToken?: string,
): Promise<INodeListSearchResult> {
const userPoolId = this.getNodeParameter('userPool', undefined, { extractValue: true }) as string;
if (!userPoolId) {
throw new NodeOperationError(this.getNode(), 'User Pool ID is required to search users');
}
const userPoolData = (await awsApiRequest.call(
this,
'POST',
'DescribeUserPool',
JSON.stringify({ UserPoolId: userPoolId }),
)) as IDataObject;
const userPool = userPoolData.UserPool as IUserPool;
const usernameAttributes = userPool.UsernameAttributes;
const responseData = (await awsApiRequest.call(
this,
'POST',
'ListUsers',
JSON.stringify({
UserPoolId: userPoolId,
Limit: 50,
NextToken: paginationToken,
}),
)) as IDataObject;
const users = responseData.Users as IUser[];
if (!users.length) {
return { results: [] };
}
const userResults = users.map((user) => {
const attributes: IUserAttribute[] = user.Attributes ?? [];
const username = user.Username;
const email = attributes.find((attr) => attr.Name === 'email')?.Value ?? '';
const phoneNumber = attributes.find((attr) => attr.Name === 'phone_number')?.Value ?? '';
const sub = attributes.find((attr) => attr.Name === 'sub')?.Value ?? '';
const name = usernameAttributes?.includes('email')
? email
: usernameAttributes?.includes('phone_number')
? phoneNumber
: username;
return { id: sub, name, value: sub };
});
return { results: formatResults(userResults, filter), paginationToken: responseData.NextToken };
}
export async function searchUserPools(
this: ILoadOptionsFunctions,
filter?: string,
paginationToken?: string,
): Promise<INodeListSearchResult> {
const responseData = (await awsApiRequest.call(
this,
'POST',
'ListUserPools',
JSON.stringify({ Limit: 50, NextToken: paginationToken }),
)) as IDataObject;
const userPools = responseData.UserPools as IUserPool[];
const userPoolsMapped = userPools.map((userPool) => ({
id: userPool.Id,
name: userPool.Name,
}));
const formattedResults = formatResults(userPoolsMapped, filter);
return { results: formattedResults, paginationToken: responseData.NextToken };
}
@@ -0,0 +1,34 @@
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
import nock from 'nock';
describe('AWS Cognito - Create Group', () => {
beforeEach(() => {
const baseUrl = 'https://cognito-idp.eu-central-1.amazonaws.com/';
nock(baseUrl)
.persist()
.defaultReplyHeaders({ 'Content-Type': 'application/x-amz-json-1.1' })
.post('/', {
UserPoolId: 'eu-central-1_qqle3XBUA',
GroupName: 'MyNewGroup11',
})
.reply(200, {
Group: {
GroupName: 'MyNewGroup11',
UserPoolId: 'eu-central-1_qqle3XBUA',
CreationDate: 1743068959.243,
LastModifiedDate: 1743068959.243,
},
});
});
new NodeTestHarness().setupTests({
workflowFiles: ['create.workflow.json'],
credentials: {
aws: {
region: 'eu-central-1',
accessKeyId: 'test',
secretAccessKey: 'test',
},
},
});
});
@@ -0,0 +1,63 @@
{
"nodes": [
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [260, 360],
"id": "9ed2b86b-7c24-4ea0-a328-92d9e6dba35a",
"name": "When clicking Execute workflow"
},
{
"parameters": {
"resource": "group",
"operation": "create",
"userPool": {
"__rl": true,
"value": "eu-central-1_qqle3XBUA",
"mode": "list",
"cachedResultName": "UserPoolThree"
},
"newGroupName": "MyNewGroup11",
"additionalFields": {},
"requestOptions": {}
},
"id": "fc6ee80d-3c8f-4ecc-a053-b4a717484c2a",
"name": "createGroup",
"type": "n8n-nodes-base.awsCognito",
"typeVersion": 1,
"position": [500, 360],
"credentials": {
"aws": {
"id": "testId",
"name": "AWS account Central Europe"
}
}
}
],
"pinData": {
"createGroup": [
{
"json": {
"CreationDate": 1743068959.243,
"GroupName": "MyNewGroup11",
"LastModifiedDate": 1743068959.243,
"UserPoolId": "eu-central-1_qqle3XBUA"
}
}
]
},
"connections": {
"When clicking Execute workflow": {
"main": [
[
{
"node": "createGroup",
"type": "main",
"index": 0
}
]
]
}
}
}
@@ -0,0 +1,35 @@
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
import nock from 'nock';
describe('AWS Cognito - Delete Group', () => {
beforeEach(() => {
const baseUrl = 'https://cognito-idp.eu-central-1.amazonaws.com/';
nock(baseUrl)
.persist()
.defaultReplyHeaders({ 'Content-Type': 'application/x-amz-json-1.1' })
.post('/', {
UserPoolId: 'eu-central-1_qqle3XBUA',
GroupName: 'MyNewGroup22',
})
.matchHeader('x-amz-target', 'AWSCognitoIdentityProviderService.DeleteGroup')
.reply(200, {
Group: {
GroupName: 'MyNewGroup22',
UserPoolId: 'eu-central-1_qqle3XBUA',
CreationDate: 1743068959.243,
LastModifiedDate: 1743068959.243,
},
});
});
new NodeTestHarness().setupTests({
workflowFiles: ['delete.workflow.json'],
credentials: {
aws: {
region: 'eu-central-1',
accessKeyId: 'test',
secretAccessKey: 'test',
},
},
});
});
@@ -0,0 +1,44 @@
{
"nodes": [
{
"parameters": {
"resource": "group",
"operation": "delete",
"userPool": {
"__rl": true,
"value": "eu-central-1_qqle3XBUA",
"mode": "list",
"cachedResultName": "UserPoolThree"
},
"group": {
"__rl": true,
"value": "MyNewGroup22",
"mode": "list",
"cachedResultName": "MyNewGroup22"
},
"requestOptions": {}
},
"type": "n8n-nodes-base.awsCognito",
"typeVersion": 1,
"position": [200, 1300],
"id": "ece9e04f-305c-45ed-afc6-34d34303f8b0",
"name": "AWS Cognito1",
"credentials": {
"aws": {
"id": "testId",
"name": "AWS account Central Europe"
}
}
}
],
"connections": {},
"pinData": {
"AWS Cognito1": [
{
"json": {
"deleted": true
}
}
]
}
}
@@ -0,0 +1,86 @@
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
import nock from 'nock';
describe('AWS Cognito - Get Group', () => {
beforeEach(() => {
const baseUrl = 'https://cognito-idp.eu-central-1.amazonaws.com/';
nock(baseUrl)
.persist()
.defaultReplyHeaders({ 'Content-Type': 'application/x-amz-json-1.1' })
.post('/', {
UserPoolId: 'eu-central-1_qqle3XBUA',
})
.matchHeader('x-amz-target', 'AWSCognitoIdentityProviderService.DescribeUserPool')
.reply(200, {
UserPool: {
Arn: 'arn:aws:cognito-idp:eu-central-1:123456789012:userpool/eu-central-1_qqle3XBUA',
CreationDate: 1739530218.869,
DeletionProtection: 'INACTIVE',
EstimatedNumberOfUsers: 4,
Id: 'eu-central-1_qqle3XBUA',
LastModifiedDate: 1739530218.869,
MfaConfiguration: 'OFF',
Name: 'UserPoolThree',
},
});
nock(baseUrl)
.persist()
.defaultReplyHeaders({ 'Content-Type': 'application/x-amz-json-1.1' })
.post('/', {
UserPoolId: 'eu-central-1_qqle3XBUA',
Limit: 50,
})
.matchHeader('x-amz-target', 'AWSCognitoIdentityProviderService.ListGroups')
.reply(200, {
Groups: [
{
CreationDate: 1741609269.287,
GroupName: 'MyNewGroup2',
LastModifiedDate: 1741609269.287,
UserPoolId: 'eu-central-1_qqle3XBUA',
},
],
});
nock(baseUrl)
.persist()
.defaultReplyHeaders({ 'Content-Type': 'application/x-amz-json-1.1' })
.post('/', {
UserPoolId: 'eu-central-1_qqle3XBUA',
GroupName: 'MyNewGroup2',
Limit: 50,
})
.matchHeader('x-amz-target', 'AWSCognitoIdentityProviderService.ListUsersInGroup')
.reply(200, {
Users: [],
});
nock(baseUrl)
.persist()
.defaultReplyHeaders({ 'Content-Type': 'application/x-amz-json-1.1' })
.post('/', {
UserPoolId: 'eu-central-1_qqle3XBUA',
GroupName: 'MyNewGroup2',
})
.matchHeader('x-amz-target', 'AWSCognitoIdentityProviderService.GetGroup')
.reply(200, {
Group: {
CreationDate: 1741609269.287,
GroupName: 'MyNewGroup2',
LastModifiedDate: 1741609269.287,
UserPoolId: 'eu-central-1_qqle3XBUA',
},
});
});
new NodeTestHarness().setupTests({
workflowFiles: ['get.workflow.json'],
credentials: {
aws: {
region: 'eu-central-1',
accessKeyId: 'test',
secretAccessKey: 'test',
},
},
});
});
@@ -0,0 +1,47 @@
{
"nodes": [
{
"parameters": {
"resource": "group",
"operation": "get",
"userPool": {
"__rl": true,
"value": "eu-central-1_qqle3XBUA",
"mode": "list",
"cachedResultName": "UserPoolThree"
},
"group": {
"__rl": true,
"value": "MyNewGroup2",
"mode": "list",
"cachedResultName": "MyNewGroup2"
},
"requestOptions": {}
},
"type": "n8n-nodes-base.awsCognito",
"typeVersion": 1,
"position": [200, 1300],
"id": "ece9e04f-305c-45ed-afc6-34d34303f8b0",
"name": "AWS Cognito1",
"credentials": {
"aws": {
"id": "testId",
"name": "AWS account Central Europe"
}
}
}
],
"connections": {},
"pinData": {
"AWS Cognito1": [
{
"json": {
"CreationDate": 1741609269.287,
"GroupName": "MyNewGroup2",
"LastModifiedDate": 1741609269.287,
"UserPoolId": "eu-central-1_qqle3XBUA"
}
}
]
}
}
@@ -0,0 +1,144 @@
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
import nock from 'nock';
describe('AWS Cognito - Get All Groups', () => {
beforeEach(() => {
const baseUrl = 'https://cognito-idp.eu-central-1.amazonaws.com/';
nock(baseUrl)
.persist()
.defaultReplyHeaders({ 'Content-Type': 'application/x-amz-json-1.1' })
.post('/', {
UserPoolId: 'eu-central-1_KkXQgdCJv',
})
.matchHeader('x-amz-target', 'AWSCognitoIdentityProviderService.ListGroups')
.reply(200, {
Groups: [
{
GroupName: 'MyNewGroup',
Description: 'Updated',
CreationDate: 1732740693.563,
LastModifiedDate: 1733422336.443,
Precedence: 0,
RoleArn: 'arn:aws:iam::123456789012:group/Admins',
UserPoolId: 'eu-central-1_KkXQgdCJv',
Users: [
{
Username: 'user1',
Attributes: [{ Name: 'email', Value: 'user1@example.com' }],
},
{
Username: 'user2',
Attributes: [{ Name: 'email', Value: 'user2@example.com' }],
},
],
},
{
GroupName: 'MyNewTesttttt',
Description: 'Updated description',
CreationDate: 1733424987.825,
LastModifiedDate: 1741609241.742,
Precedence: 5,
UserPoolId: 'eu-central-1_KkXQgdCJv',
Users: [],
},
{
GroupName: 'MyNewTest1',
Description: 'test',
CreationDate: 1733398042.783,
LastModifiedDate: 1733691256.447,
Precedence: 5,
UserPoolId: 'eu-central-1_KkXQgdCJv',
Users: [],
},
],
});
nock(baseUrl)
.persist()
.defaultReplyHeaders({ 'Content-Type': 'application/x-amz-json-1.1' })
.post('/', {
UserPoolId: 'eu-central-1_KkXQgdCJv',
GroupName: 'MyNewGroup',
})
.matchHeader('x-amz-target', 'AWSCognitoIdentityProviderService.ListUsersInGroup')
.reply(200, {
Users: [],
});
nock(baseUrl)
.persist()
.defaultReplyHeaders({ 'Content-Type': 'application/x-amz-json-1.1' })
.post('/', {
UserPoolId: 'eu-central-1_KkXQgdCJv',
GroupName: 'MyNewTesttttt',
})
.matchHeader('x-amz-target', 'AWSCognitoIdentityProviderService.ListUsersInGroup')
.reply(200, {
Users: [],
});
nock(baseUrl)
.persist()
.defaultReplyHeaders({ 'Content-Type': 'application/x-amz-json-1.1' })
.post('/', {
UserPoolId: 'eu-central-1_KkXQgdCJv',
GroupName: 'MyNewTest1',
})
.matchHeader('x-amz-target', 'AWSCognitoIdentityProviderService.ListUsersInGroup')
.reply(200, {
Users: [],
});
nock(baseUrl)
.persist()
.defaultReplyHeaders({ 'Content-Type': 'application/x-amz-json-1.1' })
.post('/', {
UserPoolId: 'eu-central-1_KkXQgdCJv',
})
.matchHeader('x-amz-target', 'AWSCognitoIdentityProviderService.ListGroups')
.reply(200, {
Groups: [
{
GroupName: 'MyNewGroup',
Description: 'Updated',
CreationDate: 1732740693.563,
LastModifiedDate: 1733422336.443,
Precedence: 0,
RoleArn: 'arn:aws:iam::123456789012:group/Admins',
UserPoolId: 'eu-central-1_KkXQgdCJv',
Users: [],
},
{
GroupName: 'MyNewTesttttt',
Description: 'Updated description',
CreationDate: 1733424987.825,
LastModifiedDate: 1741609241.742,
Precedence: 5,
UserPoolId: 'eu-central-1_KkXQgdCJv',
Users: [],
},
{
GroupName: 'MyNewTest1',
Description: 'test',
CreationDate: 1733398042.783,
LastModifiedDate: 1733691256.447,
Precedence: 5,
UserPoolId: 'eu-central-1_KkXQgdCJv',
Users: [],
},
],
});
});
new NodeTestHarness().setupTests({
workflowFiles: ['getAll.workflow.json'],
credentials: {
aws: {
region: 'eu-central-1',
accessKeyId: 'test',
secretAccessKey: 'test',
},
},
});
});
@@ -0,0 +1,89 @@
{
"nodes": [
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [260, 360],
"id": "9ed2b86b-7c24-4ea0-a328-92d9e6dba35a",
"name": "When clicking Execute workflow"
},
{
"parameters": {
"resource": "group",
"userPool": {
"__rl": true,
"value": "eu-central-1_KkXQgdCJv",
"mode": "list",
"cachedResultName": "AWS test"
},
"returnAll": true,
"includeUsers": true,
"requestOptions": {}
},
"id": "f1d96de9-a43f-4452-8760-94fc13990e0b",
"name": "getAllGroups",
"type": "n8n-nodes-base.awsCognito",
"typeVersion": 1,
"position": [460, 360],
"alwaysOutputData": true,
"credentials": {
"aws": {
"id": "testId",
"name": "AWS account Central Europe"
}
}
}
],
"pinData": {
"getAllGroups": [
{
"json": {
"CreationDate": 1732740693.563,
"Description": "Updated",
"GroupName": "MyNewGroup",
"LastModifiedDate": 1733422336.443,
"Precedence": 0,
"RoleArn": "arn:aws:iam::123456789012:group/Admins",
"UserPoolId": "eu-central-1_KkXQgdCJv",
"Users": []
}
},
{
"json": {
"CreationDate": 1733424987.825,
"Description": "Updated description",
"GroupName": "MyNewTesttttt",
"LastModifiedDate": 1741609241.742,
"Precedence": 5,
"UserPoolId": "eu-central-1_KkXQgdCJv",
"Users": []
}
},
{
"json": {
"CreationDate": 1733398042.783,
"Description": "test",
"GroupName": "MyNewTest1",
"LastModifiedDate": 1733691256.447,
"Precedence": 5,
"UserPoolId": "eu-central-1_KkXQgdCJv",
"Users": []
}
}
]
},
"connections": {
"When clicking Execute workflow": {
"main": [
[
{
"node": "getAllGroups",
"type": "main",
"index": 0
}
]
]
}
}
}
@@ -0,0 +1,36 @@
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
import nock from 'nock';
describe('AWS Cognito - Update Group', () => {
beforeEach(() => {
const baseUrl = 'https://cognito-idp.eu-central-1.amazonaws.com/';
nock(baseUrl)
.persist()
.defaultReplyHeaders({ 'Content-Type': 'application/x-amz-json-1.1' })
.post('/', {
UserPoolId: 'eu-central-1_KkXQgdCJv',
GroupName: 'MyNewTesttttt',
Description: 'Updated description',
})
.matchHeader('x-amz-target', 'AWSCognitoIdentityProviderService.UpdateGroup')
.reply(200, {
Group: {
GroupName: 'MyNewTesttttt',
UserPoolId: 'eu-central-1_KkXQgdCJv',
Description: 'Updated description',
},
});
});
new NodeTestHarness().setupTests({
workflowFiles: ['update.workflow.json'],
credentials: {
aws: {
region: 'eu-central-1',
accessKeyId: 'test',
secretAccessKey: 'test',
},
},
});
});
@@ -0,0 +1,67 @@
{
"nodes": [
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [-80, -100],
"id": "7da2ce49-9a9d-4240-b082-ff1b12d101b1",
"name": "When clicking Execute workflow"
},
{
"parameters": {
"resource": "group",
"operation": "update",
"userPool": {
"__rl": true,
"value": "eu-central-1_KkXQgdCJv",
"mode": "list",
"cachedResultName": "AWS test"
},
"group": {
"__rl": true,
"value": "MyNewTesttttt",
"mode": "list",
"cachedResultName": "MyNewTesttttt"
},
"additionalFields": {
"description": "Updated description"
},
"requestOptions": {}
},
"id": "fd0610c8-9af2-4ca5-943f-f0de5d897fc0",
"name": "updateGroup",
"type": "n8n-nodes-base.awsCognito",
"typeVersion": 1,
"position": [160, -100],
"credentials": {
"aws": {
"id": "testId",
"name": "AWS account Central Europe"
}
}
}
],
"connections": {
"When clicking Execute workflow": {
"main": [
[
{
"node": "updateGroup",
"type": "main",
"index": 0
}
]
]
}
},
"pinData": {
"updateGroup": [
{
"json": {
"updated": true
}
}
]
}
}
@@ -0,0 +1,701 @@
import { mock } from 'jest-mock-extended';
import type { MockProxy } from 'jest-mock-extended';
import type { IExecuteSingleFunctions, IHttpRequestOptions } from 'n8n-workflow';
import { NodeOperationError, NodeApiError } from 'n8n-workflow';
import {
getUserPool,
validateArn,
simplifyUserPool,
preSendUserFields,
preSendAttributes,
preSendDesiredDeliveryMediums,
getUsersInGroup,
simplifyUser,
getUserNameFromExistingUsers,
} from '../../helpers/utils';
import { searchUsers } from '../../methods/listSearch';
import { awsApiRequest, awsApiRequestAllItems } from '../../transport/index';
jest.mock('../../transport/index', () => ({
awsApiRequest: jest.fn(),
awsApiRequestAllItems: jest.fn(),
}));
jest.mock('../../methods/listSearch', () => ({
searchUsers: jest.fn(),
}));
describe('AWS Cognito - Helpers functions', () => {
let loadOptionsFunctions: MockProxy<IExecuteSingleFunctions>;
let mockRequestWithAuthentication: jest.Mock;
let mockReturnJsonArray: jest.Mock;
let requestOptions: IHttpRequestOptions;
beforeEach(() => {
loadOptionsFunctions = mock<IExecuteSingleFunctions>();
mockRequestWithAuthentication = jest.fn();
mockReturnJsonArray = jest.fn();
loadOptionsFunctions.helpers.httpRequestWithAuthentication = mockRequestWithAuthentication;
loadOptionsFunctions.helpers.returnJsonArray = mockReturnJsonArray;
loadOptionsFunctions.getCredentials.mockResolvedValue({
region: 'eu-central-1',
accessKeyId: 'test',
secretAccessKey: 'test',
});
});
afterEach(() => {
jest.resetAllMocks();
});
describe('getUserPool', () => {
it('should fetch the user pool information', async () => {
const userPoolId = 'eu-central-1_W3WwpiBXV';
const mockResponse = {
UserPool: {
Id: userPoolId,
Name: 'UserPoolSimple',
},
};
(awsApiRequest as jest.Mock).mockResolvedValue(mockResponse);
const userPool = await getUserPool.call(loadOptionsFunctions, userPoolId);
expect(userPool).toEqual({ Id: userPoolId, Name: 'UserPoolSimple' });
});
it('should throw an error if user pool ID is missing', async () => {
await expect(getUserPool.call(loadOptionsFunctions, '')).rejects.toThrowError(
NodeOperationError,
);
});
it('should throw an error if user pool is not found', async () => {
const userPoolId = 'invalid-user-pool-id';
(awsApiRequest as jest.Mock).mockResolvedValue({});
await expect(getUserPool.call(loadOptionsFunctions, userPoolId)).rejects.toThrowError(
NodeOperationError,
);
});
});
describe('getUsersInGroup', () => {
it('should throw an error if UserPoolId is missing', async () => {
loadOptionsFunctions.getNodeParameter.mockReturnValue(undefined);
await expect(
getUsersInGroup.call(loadOptionsFunctions, 'groupName', ''),
).rejects.toThrowError(NodeOperationError);
});
it('should return an empty list if no users are found', async () => {
(loadOptionsFunctions.getNodeParameter as jest.Mock).mockReturnValue('userPoolId');
(awsApiRequestAllItems as jest.Mock).mockResolvedValue([]);
const result = await getUsersInGroup.call(loadOptionsFunctions, 'groupName', 'userPoolId');
expect(result).toEqual([]);
});
it('should return users correctly', async () => {
const mockUsers = [
{
Username: 'user1',
Enabled: true,
Attributes: [{ Name: 'email', Value: 'user1@example.com' }],
},
{
Username: 'user2',
Enabled: true,
Attributes: [{ Name: 'email', Value: 'user2@example.com' }],
},
];
(awsApiRequestAllItems as jest.Mock).mockResolvedValue(mockUsers);
const result = await getUsersInGroup.call(loadOptionsFunctions, 'groupName', 'userPoolId');
expect(result).toEqual([
{
Username: 'user1',
Enabled: true,
Attributes: [
{
Name: 'email',
Value: 'user1@example.com',
},
],
},
{
Username: 'user2',
Enabled: true,
Attributes: [
{
Name: 'email',
Value: 'user2@example.com',
},
],
},
]);
});
it('should handle empty attributes and missing values', async () => {
const mockUsers = [
{
Username: 'user1',
Enabled: true,
Attributes: [{ Name: 'email', Value: '' }],
},
];
(awsApiRequestAllItems as jest.Mock).mockResolvedValue(mockUsers);
const result = await getUsersInGroup.call(loadOptionsFunctions, 'groupName', 'userPoolId');
expect(result).toEqual([
{
Username: 'user1',
Enabled: true,
Attributes: [
{
Name: 'email',
Value: '',
},
],
},
]);
});
});
describe('validateArn', () => {
it('should validate the ARN format', async () => {
loadOptionsFunctions.getNodeParameter.mockImplementation((name: string) => {
if (name === 'additionalFields.arn') {
return 'arn:aws:iam::123456789012:role/GroupRole';
}
return '';
});
requestOptions = {
body: { additionalFields: { arn: 'arn:aws:iam::123456789012:role/GroupRole' } },
headers: {},
url: 'example.com',
};
const result = await validateArn.call(loadOptionsFunctions, requestOptions);
expect(result).toEqual(requestOptions);
});
it('should throw an error if the ARN format is invalid', async () => {
loadOptionsFunctions.getNodeParameter.mockImplementation((name: string) => {
if (name === 'additionalFields.arn') {
return 'invalid-arn';
}
return '';
});
requestOptions = {
body: { additionalFields: { arn: 'invalid-arn' } },
headers: {},
url: 'example.com',
};
await expect(validateArn.call(loadOptionsFunctions, requestOptions)).rejects.toThrowError(
NodeApiError,
);
});
});
describe('simplifyUserPool', () => {
it('should simplify the user pool data when simple is true', async () => {
loadOptionsFunctions.getNodeParameter.mockImplementation(() => true);
const items = [{ json: { UserPool: { Id: 'userPoolId', Name: 'UserPoolName' } } }];
const result = await simplifyUserPool.call(loadOptionsFunctions, items, {
body: {},
headers: {},
statusCode: 200,
});
expect(result).toEqual([{ json: { UserPool: { Id: 'userPoolId', Name: 'UserPoolName' } } }]);
});
});
describe('simplifyUserData', () => {
it('should simplify a single user with UserAttributes when simple is true', async () => {
loadOptionsFunctions.getNodeParameter.mockReturnValue(true);
const items = [
{
json: {
UserAttributes: [{ Name: 'email', Value: 'user@example.com' }],
},
},
];
const result = await simplifyUser.call(loadOptionsFunctions, items, {
body: {},
headers: {},
statusCode: 200,
});
expect(result).toEqual([{ json: { email: 'user@example.com' } }]);
});
it('should simplify multiple users in Users array when simple is true', async () => {
loadOptionsFunctions.getNodeParameter.mockReturnValue(true);
const items = [
{
json: {
Users: [
{
Attributes: [{ Name: 'email', Value: 'user1@example.com' }],
},
],
},
},
];
const result = await simplifyUser.call(loadOptionsFunctions, items, {
body: {},
headers: {},
statusCode: 200,
});
expect(result).toEqual([
{
json: {
Users: [{ email: 'user1@example.com' }],
},
},
]);
});
it('should return original items when simple is false', async () => {
const items = [
{
json: {
UserAttributes: [{ Name: 'email', Value: 'user@example.com' }],
},
},
];
const result = await simplifyUser.call(loadOptionsFunctions, items, {
body: {},
headers: {},
statusCode: 200,
});
expect(result).toEqual(items);
});
});
describe('getUserNameFromExistingUsers', () => {
const userPoolId = 'eu-central-1_KkXQgdCJv';
const userName = '03a438f2-10d1-70f1-f45a-09753ab5c4c3';
it('should return the userName if email or phone is used for authentication', async () => {
const isEmailOrPhone = true;
const result = await getUserNameFromExistingUsers.call(
loadOptionsFunctions,
userName,
userPoolId,
isEmailOrPhone,
);
expect(result).toEqual(userName);
});
it('should return the username from ListUsers API when it is not email or phone authentication', async () => {
const isEmailOrPhone = false;
const mockApiResponse = {
Users: [{ Username: 'existing-user' }],
};
(awsApiRequest as jest.Mock).mockResolvedValue(mockApiResponse);
const result = await getUserNameFromExistingUsers.call(
loadOptionsFunctions,
userName,
userPoolId,
isEmailOrPhone,
);
expect(result).toEqual('existing-user');
});
it('should return undefined if no user is found in ListUsers API', async () => {
const isEmailOrPhone = false;
const mockApiResponse = {
Users: [],
};
(awsApiRequest as jest.Mock).mockResolvedValue(mockApiResponse);
const result = await getUserNameFromExistingUsers.call(
loadOptionsFunctions,
userName,
userPoolId,
isEmailOrPhone,
);
expect(result).toBeUndefined();
});
});
describe('preSendUserFields', () => {
beforeEach(() => {
loadOptionsFunctions.getNodeParameter.mockImplementation((name: string) => {
if (name === 'operation') return 'create';
if (name === 'userPool') return 'test-pool-id';
if (name === 'newUserName') return 'test@example.com';
return undefined;
});
});
it('should return the request body with the correct username when operation is "create" and valid email is provided', async () => {
requestOptions = {
body: JSON.stringify({ someField: 'value' }),
method: 'POST',
url: '',
headers: {},
};
(awsApiRequest as jest.Mock).mockResolvedValue({
UserPool: { UsernameAttributes: ['email'] },
});
const result = await preSendUserFields.call(loadOptionsFunctions, requestOptions);
expect(result.body).toEqual(
JSON.stringify({
someField: 'value',
Username: 'test@example.com',
}),
);
});
it('should return the request body with the correct username when operation is "update" and user exists', async () => {
requestOptions = {
body: JSON.stringify({ someField: 'value' }),
method: 'POST',
url: '',
headers: {},
};
loadOptionsFunctions.getNodeParameter.mockImplementation((name: string) => {
if (name === 'operation') return 'update';
if (name === 'user') return 'existing-user';
if (name === 'userPool') return 'eu-central-1_KkXQgdCJv';
return undefined;
});
(awsApiRequest as jest.Mock).mockResolvedValue({
UserPool: { UsernameAttributes: ['email'] },
});
(searchUsers as jest.Mock).mockResolvedValue({
results: [{ name: 'existing-user', value: 'existing-user' }],
});
const result = await preSendUserFields.call(loadOptionsFunctions, requestOptions);
expect(result.body).toEqual(
JSON.stringify({
someField: 'value',
Username: 'existing-user',
}),
);
});
it('should return the request body without a username when operation is "update" and no user is found', async () => {
requestOptions = {
body: JSON.stringify({ someField: 'value' }),
method: 'POST',
url: '',
headers: {},
};
loadOptionsFunctions.getNodeParameter.mockImplementationOnce((name: string) => {
if (name === 'operation') return 'update';
if (name === 'user') return 'non-existing-user';
return undefined;
});
(awsApiRequest as jest.Mock).mockResolvedValue({
UserPool: { UsernameAttributes: ['email'] },
});
(searchUsers as jest.Mock).mockResolvedValue({
results: [],
});
const result = await preSendUserFields.call(loadOptionsFunctions, requestOptions);
expect(result.body).toEqual(
JSON.stringify({
someField: 'value',
}),
);
});
});
describe('preSendAttributes', () => {
beforeEach(() => {
requestOptions = {
body: JSON.stringify({ someField: 'value' }),
method: 'POST',
url: '',
headers: {},
};
});
it('should throw an error if no user attributes are provided (update operation)', async () => {
loadOptionsFunctions.getNodeParameter.mockImplementation((name: string) => {
if (name === 'operation') return 'update';
if (name === 'userAttributes.attributes') return [];
return undefined;
});
await expect(
preSendAttributes.call(loadOptionsFunctions, requestOptions),
).rejects.toThrowError(
new NodeOperationError(loadOptionsFunctions.getNode(), 'No user attributes provided', {
description: 'At least one user attribute must be provided for the update operation.',
}),
);
});
it('should throw an error if a user attribute is invalid (empty value) (create operation)', async () => {
loadOptionsFunctions.getNodeParameter.mockImplementation((name: string) => {
if (name === 'operation') return 'create';
if (name === 'additionalFields.userAttributes.attributes') {
return [{ attributeType: 'standard', standardName: 'email', value: '' }];
}
return undefined;
});
await expect(
preSendAttributes.call(loadOptionsFunctions, requestOptions),
).rejects.toThrowError(
new NodeOperationError(loadOptionsFunctions.getNode(), 'Invalid User Attribute', {
description: 'Each attribute must have a valid name and value.',
}),
);
});
it('should throw an error if email_verified is true but email is missing (create operation)', async () => {
loadOptionsFunctions.getNodeParameter.mockImplementation((name: string) => {
if (name === 'operation') return 'create';
if (name === 'additionalFields.userAttributes.attributes') {
return [{ attributeType: 'standard', standardName: 'email_verified', value: 'true' }];
}
return undefined;
});
await expect(
preSendAttributes.call(loadOptionsFunctions, requestOptions),
).rejects.toThrowError(
new NodeOperationError(
loadOptionsFunctions.getNode(),
'Missing required "email" attribute',
{
description:
'"email_verified" is set to true, but the corresponding "email" attribute is not provided.',
},
),
);
});
it('should throw an error if phone_number_verified is true but phone_number is missing (create operation)', async () => {
loadOptionsFunctions.getNodeParameter.mockImplementation((name: string) => {
if (name === 'operation') return 'create';
if (name === 'additionalFields.userAttributes.attributes') {
return [
{ attributeType: 'standard', standardName: 'phone_number_verified', value: 'true' },
];
}
return undefined;
});
await expect(
preSendAttributes.call(loadOptionsFunctions, requestOptions),
).rejects.toThrowError(
new NodeOperationError(
loadOptionsFunctions.getNode(),
'Missing required "phone_number" attribute',
{
description:
'"phone_number_verified" is set to true, but the corresponding "phone_number" attribute is not provided.',
},
),
);
});
it('should add the user attribute to the body when valid (create operation)', async () => {
loadOptionsFunctions.getNodeParameter.mockImplementation((name: string) => {
if (name === 'operation') return 'create';
if (name === 'additionalFields.userAttributes.attributes') {
return [
{ attributeType: 'standard', standardName: 'email', value: 'test@example.com' },
{ attributeType: 'standard', standardName: 'phone_number', value: '1234567890' },
];
}
return undefined;
});
const result = await preSendAttributes.call(loadOptionsFunctions, requestOptions);
expect(result.body).toEqual(
JSON.stringify({
someField: 'value',
UserAttributes: [
{ Name: 'email', Value: 'test@example.com' },
{ Name: 'phone_number', Value: '1234567890' },
],
}),
);
});
it('should correctly process custom attributes (create operation)', async () => {
loadOptionsFunctions.getNodeParameter.mockImplementation((name: string) => {
if (name === 'operation') return 'create';
if (name === 'additionalFields.userAttributes.attributes') {
return [
{
attributeType: 'custom',
customName: 'custom_attr',
value: 'custom_value',
},
];
}
return undefined;
});
const result = await preSendAttributes.call(loadOptionsFunctions, requestOptions);
expect(result.body).toEqual(
JSON.stringify({
someField: 'value',
UserAttributes: [{ Name: 'custom:custom_attr', Value: 'custom_value' }],
}),
);
});
it('should throw an error if a custom attribute has no name or value (create operation)', async () => {
loadOptionsFunctions.getNodeParameter.mockImplementation((name: string) => {
if (name === 'operation') return 'create';
if (name === 'additionalFields.userAttributes.attributes') {
return [{ attributeType: 'custom', customName: '', value: '' }];
}
return undefined;
});
await expect(
preSendAttributes.call(loadOptionsFunctions, requestOptions),
).rejects.toThrowError(
new NodeOperationError(loadOptionsFunctions.getNode(), 'Invalid User Attribute', {
description: 'Each attribute must have a valid name and value.',
}),
);
});
});
describe('preSendDesiredDeliveryMediums', () => {
beforeEach(() => {
requestOptions = { body: {}, url: '' };
});
it('should not throw an error if EMAIL is selected and email is provided', async () => {
loadOptionsFunctions.getNodeParameter.mockImplementation((name: string) => {
if (name === 'additionalFields.desiredDeliveryMediums') {
return ['EMAIL'];
}
if (name === 'additionalFields.userAttributes.attributes') {
return [
{ standardName: 'email', value: 'test@example.com' },
{ standardName: 'phone_number', value: '1234567890' },
];
}
return undefined;
});
await expect(
preSendDesiredDeliveryMediums.call(loadOptionsFunctions, requestOptions),
).resolves.toEqual(requestOptions);
});
it('should throw an error if EMAIL is selected but email is missing', async () => {
loadOptionsFunctions.getNodeParameter.mockImplementation((name: string) => {
if (name === 'additionalFields.desiredDeliveryMediums') {
return ['EMAIL'];
}
if (name === 'additionalFields.userAttributes.attributes') {
return [{ standardName: 'phone_number', value: '1234567890' }];
}
return undefined;
});
await expect(
preSendDesiredDeliveryMediums.call(loadOptionsFunctions, requestOptions),
).rejects.toThrowError('Missing required "email" attribute');
});
it('should not throw an error if SMS is selected and phone number is provided', async () => {
loadOptionsFunctions.getNodeParameter.mockImplementation((name: string) => {
if (name === 'additionalFields.desiredDeliveryMediums') {
return ['SMS'];
}
if (name === 'additionalFields.userAttributes.attributes') {
return [
{ standardName: 'email', value: 'test@example.com' },
{ standardName: 'phone_number', value: '1234567890' },
];
}
return undefined;
});
await expect(
preSendDesiredDeliveryMediums.call(loadOptionsFunctions, requestOptions),
).resolves.toEqual(requestOptions);
});
it('should throw an error if SMS is selected but phone number is missing', async () => {
loadOptionsFunctions.getNodeParameter.mockImplementation((name: string) => {
if (name === 'additionalFields.desiredDeliveryMediums') {
return ['SMS'];
}
if (name === 'additionalFields.userAttributes.attributes') {
return [{ standardName: 'email', value: 'test@example.com' }];
}
return undefined;
});
await expect(
preSendDesiredDeliveryMediums.call(loadOptionsFunctions, requestOptions),
).rejects.toThrowError('Missing required "phone_number" attribute');
});
it('should not throw an error if both EMAIL and SMS are selected and both attributes are provided', async () => {
loadOptionsFunctions.getNodeParameter.mockImplementation((name: string) => {
if (name === 'additionalFields.desiredDeliveryMediums') {
return ['EMAIL', 'SMS'];
}
if (name === 'additionalFields.userAttributes.attributes') {
return [
{ standardName: 'email', value: 'test@example.com' },
{ standardName: 'phone_number', value: '1234567890' },
];
}
return undefined;
});
await expect(
preSendDesiredDeliveryMediums.call(loadOptionsFunctions, requestOptions),
).resolves.toEqual(requestOptions);
});
});
});
@@ -0,0 +1,252 @@
import {
ApplicationError,
type ILoadOptionsFunctions,
type INodeListSearchResult,
} from 'n8n-workflow';
import type { IUserPool } from '../../helpers/interfaces';
import {
searchUsers,
searchGroups,
searchUserPools,
searchGroupsForUser,
} from '../../methods/listSearch';
import { awsApiRequest, awsApiRequestAllItems } from '../../transport/index';
jest.mock('../../transport/index', () => ({
awsApiRequest: jest.fn(),
awsApiRequestAllItems: jest.fn(),
}));
describe('AWS Cognito Functions', () => {
describe('searchUsers', () => {
it('should return user results when users are found', async () => {
const mockDescribeUserPoolResponse = {
UserPool: {
UsernameAttributes: ['email'],
},
};
const mockResponse = {
Users: [
{
Username: 'User1',
Attributes: [
{ Name: 'email', Value: 'user1@example.com' },
{ Name: 'phone_number', Value: '1234567890' },
{ Name: 'sub', Value: 'sub1' },
],
},
{
Username: 'User2',
Attributes: [
{ Name: 'email', Value: 'user2@example.com' },
{ Name: 'phone_number', Value: '9876543210' },
{ Name: 'sub', Value: 'sub2' },
],
},
],
NextToken: 'next-token',
};
(awsApiRequest as jest.Mock)
.mockResolvedValueOnce(mockDescribeUserPoolResponse)
.mockResolvedValueOnce(mockResponse);
const mockContext = {
getNodeParameter: jest.fn((param) => {
if (param === 'userPool') {
return 'user-pool-id';
}
return null;
}),
} as unknown as ILoadOptionsFunctions;
const result = await searchUsers.call(mockContext, '', '');
const expectedResult: INodeListSearchResult = {
results: [
{ name: 'user1@example.com', value: 'sub1' },
{ name: 'user2@example.com', value: 'sub2' },
],
paginationToken: 'next-token',
};
expect(result).toEqual(expectedResult);
expect(awsApiRequest).toHaveBeenCalledWith(
'POST',
'ListUsers',
expect.stringContaining('UserPoolId'),
);
});
it('should throw an error if UserPoolId is missing', async () => {
const mockContext = {
getNodeParameter: jest.fn().mockReturnValue(''),
getNode: jest.fn(),
} as unknown as ILoadOptionsFunctions;
await expect(searchUsers.call(mockContext)).rejects.toThrow(
'User Pool ID is required to search users',
);
});
});
describe('searchGroups', () => {
it('should return group results when groups are found', async () => {
const mockResponse = {
Groups: [
{ GroupName: 'Group1', UserPoolId: 'user-pool-id' },
{ GroupName: 'Group2', UserPoolId: 'user-pool-id' },
],
NextToken: 'next-token',
};
(awsApiRequest as jest.Mock).mockResolvedValue(mockResponse);
const mockContext = {
getNodeParameter: jest.fn((param) => {
if (param === 'userPool') {
return { value: 'user-pool-id' };
}
return null;
}),
} as unknown as ILoadOptionsFunctions;
const result = await searchGroups.call(mockContext, '', '');
const expectedResult: INodeListSearchResult = {
results: [
{ name: 'Group1', value: 'Group1' },
{ name: 'Group2', value: 'Group2' },
],
paginationToken: 'next-token',
};
expect(result).toEqual(expectedResult);
expect(awsApiRequest).toHaveBeenCalledWith(
'POST',
'ListGroups',
expect.stringContaining('UserPoolId'),
);
});
it('should throw an error if UserPoolId is missing', async () => {
const mockContext = {
getNodeParameter: jest.fn().mockReturnValue(null),
getNode: jest.fn(),
} as unknown as ILoadOptionsFunctions;
await expect(searchGroups.call(mockContext)).rejects.toThrow(ApplicationError);
});
});
describe('searchUserPools', () => {
it('should return user pool results when user pools are found', async () => {
const mockResponse = {
UserPools: [
{ Id: 'pool1', Name: 'User Pool 1' },
{ Id: 'pool2', Name: 'User Pool 2' },
],
NextToken: 'next-token',
};
(awsApiRequest as jest.Mock).mockResolvedValue(mockResponse);
const mockContext = {
getNodeParameter: jest.fn((param) => {
if (param === 'userPool') {
return { value: 'user-pool-id' };
}
return null;
}),
} as unknown as ILoadOptionsFunctions;
const result = await searchUserPools.call(mockContext, '', '');
const expectedResult: INodeListSearchResult = {
results: [
{ name: 'UserPool1', value: 'pool1' },
{ name: 'UserPool2', value: 'pool2' },
],
paginationToken: 'next-token',
};
expect(result).toEqual(expectedResult);
expect(awsApiRequest).toHaveBeenCalledWith(
'POST',
'ListUserPools',
expect.stringContaining('Limit'),
);
});
});
describe('searchGroupsForUser', () => {
const userName = '03a438f2-10d1-70f1-f45a-09753ab5c4c3';
const userPoolId = 'eu-central-1_KkXQgdCJv';
const mockContext = {
getNodeParameter: jest.fn((param) => {
if (param === 'user') return userName;
if (param === 'userPool') return userPoolId;
return null;
}),
getNode: jest.fn(() => ({
name: 'mockNode',
})),
} as unknown as ILoadOptionsFunctions;
beforeEach(() => {
(awsApiRequest as jest.Mock).mockResolvedValueOnce({
UserPool: {
Id: userPoolId,
UsernameAttributes: ['email', 'phone_number'],
},
} as { UserPool: IUserPool });
});
it('should handle empty groups response', async () => {
(awsApiRequestAllItems as jest.Mock).mockResolvedValueOnce([]);
const result = await searchGroupsForUser.call(mockContext, '');
const expectedResult: INodeListSearchResult = {
results: [],
};
expect(result).toEqual(expectedResult);
});
it('should return filtered and sorted group list', async () => {
const mockGroups = [
{ GroupName: 'Developers' },
{ GroupName: 'Admins' },
{ GroupName: 'Guests' },
];
(awsApiRequestAllItems as jest.Mock).mockResolvedValueOnce(mockGroups);
const result = await searchGroupsForUser.call(mockContext, 'dev');
expect(result).toEqual({
results: [{ name: 'Developers', value: 'Developers' }],
});
});
it('should return all groups when no filter is passed', async () => {
const mockGroups = [{ GroupName: 'Zeta' }, { GroupName: 'Alpha' }, { GroupName: 'Beta' }];
(awsApiRequestAllItems as jest.Mock).mockResolvedValueOnce(mockGroups);
const result = await searchGroupsForUser.call(mockContext);
expect(result).toEqual({
results: [
{ name: 'Alpha', value: 'Alpha' },
{ name: 'Beta', value: 'Beta' },
{ name: 'Zeta', value: 'Zeta' },
],
});
});
});
});
@@ -0,0 +1,74 @@
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
import nock from 'nock';
describe('AWS Cognito - Add User to Group', () => {
beforeEach(() => {
const baseUrl = 'https://cognito-idp.eu-central-1.amazonaws.com/';
nock(baseUrl)
.persist()
.defaultReplyHeaders({ 'Content-Type': 'application/x-amz-json-1.1' })
.post('/', {
UserPoolId: 'eu-central-1_W3WwpiBXV',
})
.matchHeader('x-amz-target', 'AWSCognitoIdentityProviderService.DescribeUserPool')
.reply(200, {
UserPool: {
Arn: 'arn:aws:cognito-idp:eu-central-1:130450532146:userpool/eu-central-1_W3WwpiBXV',
CreationDate: 1739530218.869,
DeletionProtection: 'INACTIVE',
EstimatedNumberOfUsers: 4,
Id: 'eu-central-1_W3WwpiBXV',
LastModifiedDate: 1739530218.869,
MfaConfiguration: 'OFF',
Name: 'UserPoolSimple',
},
});
nock(baseUrl)
.persist()
.defaultReplyHeaders({ 'Content-Type': 'application/x-amz-json-1.1' })
.post('/', {
UserPoolId: 'eu-central-1_W3WwpiBXV',
Filter: 'sub = "0394e8e2-5081-7020-06bd-44bdfc84dd10"',
})
.matchHeader('x-amz-target', 'AWSCognitoIdentityProviderService.ListUsers')
.reply(200, {
Users: [
{
Username: '0394e8e2-5081-7020-06bd-44bdfc84dd10',
Attributes: [
{ Name: 'email', Value: 'UserSimple' },
{ Name: 'Sub', Value: '0394e8e2-5081-7020-06bd-44bdfc84dd10' },
{ Name: 'Enabled', Value: true },
{ Name: 'UserCreateDate', Value: 1736343033.226 },
{ Name: 'UserLastModifiedDate', Value: 1736343033.226 },
{ Name: 'UserStatus', Value: 'FORCE_CHANGE_PASSWORD' },
],
},
],
});
nock(baseUrl)
.persist()
.defaultReplyHeaders({ 'Content-Type': 'application/x-amz-json-1.1' })
.post('/', {
UserPoolId: 'eu-central-1_W3WwpiBXV',
Username: '0394e8e2-5081-7020-06bd-44bdfc84dd10',
GroupName: 'MyNewGroupSimple',
})
.matchHeader('x-amz-target', 'AWSCognitoIdentityProviderService.AdminAddUserToGroup')
.reply(200, {});
});
new NodeTestHarness().setupTests({
workflowFiles: ['addToGroup.workflow.json'],
credentials: {
aws: {
region: 'eu-central-1',
accessKeyId: 'test',
secretAccessKey: 'test',
},
},
});
});
@@ -0,0 +1,69 @@
{
"nodes": [
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [-80, -100],
"id": "7da2ce49-9a9d-4240-b082-ff1b12d101b1",
"name": "When clicking Execute workflow"
},
{
"parameters": {
"operation": "addToGroup",
"userPool": {
"__rl": true,
"value": "eu-central-1_W3WwpiBXV",
"mode": "list",
"cachedResultName": "UserPoolSimple"
},
"user": {
"__rl": true,
"value": "0394e8e2-5081-7020-06bd-44bdfc84dd10",
"mode": "list",
"cachedResultName": "UserSimple"
},
"group": {
"__rl": true,
"value": "MyNewGroupSimple",
"mode": "list",
"cachedResultName": "MyNewGroupSimple"
},
"requestOptions": {}
},
"type": "n8n-nodes-base.awsCognito",
"typeVersion": 1,
"position": [100, -100],
"id": "0accb395-3cdb-4c3f-8adc-1a47567c37b5",
"name": "AWS Cognito",
"credentials": {
"aws": {
"id": "testId",
"name": "AWS account Central Europe"
}
}
}
],
"connections": {
"When clicking Execute workflow": {
"main": [
[
{
"node": "AWS Cognito",
"type": "main",
"index": 0
}
]
]
}
},
"pinData": {
"AWS Cognito": [
{
"json": {
"addedToGroup": true
}
}
]
}
}
@@ -0,0 +1,57 @@
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
import nock from 'nock';
describe('AWS Cognito - Create User', () => {
beforeEach(() => {
const baseUrl = 'https://cognito-idp.eu-central-1.amazonaws.com/';
nock(baseUrl)
.persist()
.defaultReplyHeaders({ 'Content-Type': 'application/x-amz-json-1.1' })
.post('/', {
UserPoolId: 'eu-central-1_W3WwpiBXV',
})
.matchHeader('x-amz-target', 'AWSCognitoIdentityProviderService.DescribeUserPool')
.reply(200, {
UserPool: {
Id: 'eu-central-1_W3WwpiBXV',
Name: 'MyUserPool',
CreationDate: 1627891230,
},
});
nock(baseUrl)
.persist()
.defaultReplyHeaders({ 'Content-Type': 'application/x-amz-json-1.1' })
.post('/', {
UserPoolId: 'eu-central-1_W3WwpiBXV',
Username: 'Johnn12',
})
.matchHeader('x-amz-target', 'AWSCognitoIdentityProviderService.AdminCreateUser')
.reply(200, {
User: {
Username: 'Johnn12',
UserStatus: 'FORCE_CHANGE_PASSWORD',
Attributes: [
{
Name: 'sub',
Value: '03d43812-00c1-7098-075e-8a535fdefc1b',
},
],
UserCreateDate: 1743068750.761,
UserLastModifiedDate: 1743068750.761,
Enabled: true,
},
});
});
new NodeTestHarness().setupTests({
workflowFiles: ['create.workflow.json'],
credentials: {
aws: {
region: 'eu-central-1',
accessKeyId: 'test',
secretAccessKey: 'test',
},
},
});
});
@@ -0,0 +1,70 @@
{
"nodes": [
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [260, 360],
"id": "9ed2b86b-7c24-4ea0-a328-92d9e6dba35a",
"name": "When clicking Execute workflow"
},
{
"parameters": {
"operation": "create",
"userPool": {
"__rl": true,
"value": "eu-central-1_W3WwpiBXV",
"mode": "list",
"cachedResultName": "UserPoolSimple"
},
"newUserName": "Johnn12",
"additionalFields": {},
"requestOptions": {}
},
"id": "ea53747b-2354-486c-9a9d-bac3dd88bacb",
"name": "createUser",
"type": "n8n-nodes-base.awsCognito",
"typeVersion": 1,
"position": [460, 360],
"alwaysOutputData": true,
"credentials": {
"aws": {
"id": "testId",
"name": "AWS account Central Europe"
}
}
}
],
"pinData": {
"createUser": [
{
"json": {
"Attributes": [
{
"Name": "sub",
"Value": "03d43812-00c1-7098-075e-8a535fdefc1b"
}
],
"Enabled": true,
"UserCreateDate": 1743068750.761,
"UserLastModifiedDate": 1743068750.761,
"UserStatus": "FORCE_CHANGE_PASSWORD",
"Username": "Johnn12"
}
}
]
},
"connections": {
"When clicking Execute workflow": {
"main": [
[
{
"node": "createUser",
"type": "main",
"index": 0
}
]
]
}
}
}
@@ -0,0 +1,74 @@
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
import nock from 'nock';
describe('AWS Cognito - Delete User', () => {
beforeEach(() => {
const baseUrl = 'https://cognito-idp.eu-central-1.amazonaws.com/';
nock(baseUrl)
.persist()
.defaultReplyHeaders({ 'Content-Type': 'application/x-amz-json-1.1' })
.post('/', {
UserPoolId: 'eu-central-1_EUZ4iEF1T',
})
.matchHeader('x-amz-target', 'AWSCognitoIdentityProviderService.DescribeUserPool')
.reply(200, {
UserPool: {
Arn: 'arn:aws:cognito-idp:eu-central-1:130450532146:userpool/eu-central-1_EUZ4iEF1T',
CreationDate: 1739530218.869,
DeletionProtection: 'INACTIVE',
EstimatedNumberOfUsers: 4,
Id: 'eu-central-1_EUZ4iEF1T',
LastModifiedDate: 1739530218.869,
MfaConfiguration: 'OFF',
Name: 'UserPoolTwo',
},
});
nock(baseUrl)
.persist()
.defaultReplyHeaders({ 'Content-Type': 'application/x-amz-json-1.1' })
.post('/', {
UserPoolId: 'eu-central-1_EUZ4iEF1T',
Filter: 'sub = "53c4f8c2-c071-707b-debd-d45585618da0"',
})
.matchHeader('x-amz-target', 'AWSCognitoIdentityProviderService.ListUsers')
.reply(200, {
Users: [
{
Username: '53c4f8c2-c071-707b-debd-d45585618da0',
Attributes: [
{ Name: 'email', Value: 'UserSimple' },
{ Name: 'Sub', Value: '53c4f8c2-c071-707b-debd-d45585618da0' },
{ Name: 'Enabled', Value: true },
{ Name: 'UserCreateDate', Value: 1736343033.226 },
{ Name: 'UserLastModifiedDate', Value: 1736343033.226 },
{ Name: 'UserStatus', Value: 'FORCE_CHANGE_PASSWORD' },
],
},
],
});
nock(baseUrl)
.persist()
.defaultReplyHeaders({ 'Content-Type': 'application/x-amz-json-1.1' })
.post('/', {
UserPoolId: 'eu-central-1_EUZ4iEF1T',
Username: '53c4f8c2-c071-707b-debd-d45585618da0',
})
.matchHeader('x-amz-target', 'AWSCognitoIdentityProviderService.AdminDeleteUser')
.reply(200, {
Message: 'User successfully deleted',
});
});
new NodeTestHarness().setupTests({
workflowFiles: ['delete.workflow.json'],
credentials: {
aws: {
region: 'eu-central-1',
accessKeyId: 'test',
secretAccessKey: 'test',
},
},
});
});
@@ -0,0 +1,63 @@
{
"nodes": [
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [-80, -100],
"id": "7da2ce49-9a9d-4240-b082-ff1b12d101b1",
"name": "When clicking Execute workflow"
},
{
"parameters": {
"operation": "delete",
"userPool": {
"__rl": true,
"value": "eu-central-1_EUZ4iEF1T",
"mode": "list",
"cachedResultName": "UserPoolTwo"
},
"user": {
"__rl": true,
"value": "53c4f8c2-c071-707b-debd-d45585618da0",
"mode": "list",
"cachedResultName": "userName12"
},
"requestOptions": {}
},
"type": "n8n-nodes-base.awsCognito",
"typeVersion": 1,
"position": [0, 40],
"id": "bc3483ef-4922-4759-a51d-12e5bd996628",
"name": "AWS Cognito",
"credentials": {
"aws": {
"id": "testId",
"name": "AWS account Central Europe"
}
}
}
],
"connections": {
"When clicking Execute workflow": {
"main": [
[
{
"node": "AWS Cognito",
"type": "main",
"index": 0
}
]
]
}
},
"pinData": {
"AWS Cognito": [
{
"json": {
"deleted": true
}
}
]
}
}
@@ -0,0 +1,83 @@
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
import nock from 'nock';
describe('AWS Cognito - Get User', () => {
beforeEach(() => {
const baseUrl = 'https://cognito-idp.eu-central-1.amazonaws.com/';
nock(baseUrl)
.persist()
.defaultReplyHeaders({ 'Content-Type': 'application/x-amz-json-1.1' })
.post('/', {
UserPoolId: 'eu-central-1_EUZ4iEF1T',
})
.matchHeader('x-amz-target', 'AWSCognitoIdentityProviderService.DescribeUserPool')
.reply(200, {
UserPool: {
Arn: 'arn:aws:cognito-idp:eu-central-1:130450532146:userpool/eu-central-1_EUZ4iEF1T',
CreationDate: 1739530218.869,
DeletionProtection: 'INACTIVE',
EstimatedNumberOfUsers: 4,
Id: 'eu-central-1_EUZ4iEF1T',
LastModifiedDate: 1739530218.869,
MfaConfiguration: 'OFF',
Name: 'UserPoolSimple',
},
});
nock(baseUrl)
.persist()
.defaultReplyHeaders({ 'Content-Type': 'application/x-amz-json-1.1' })
.post('/', {
UserPoolId: 'eu-central-1_EUZ4iEF1T',
Filter: 'sub = "b30498c2-d0f1-70a8-4b0c-3da25a3b998f"',
})
.matchHeader('x-amz-target', 'AWSCognitoIdentityProviderService.ListUsers')
.reply(200, {
Users: [
{
Username: 'userName10',
UserAttributes: [
{ Name: 'sub', Value: 'b30498c2-d0f1-70a8-4b0c-3da25a3b998f' },
{ Name: 'family_name', Value: 'New FamilyName 2' },
],
UserCreateDate: 1744206331.569,
UserLastModifiedDate: 1744206366.034,
Enabled: true,
UserStatus: 'FORCE_CHANGE_PASSWORD',
},
],
});
nock(baseUrl)
.persist()
.defaultReplyHeaders({ 'Content-Type': 'application/x-amz-json-1.1' })
.post('/', {
UserPoolId: 'eu-central-1_EUZ4iEF1T',
Username: 'userName10',
})
.matchHeader('x-amz-target', 'AWSCognitoIdentityProviderService.AdminGetUser')
.reply(200, {
Username: 'userName10',
UserAttributes: [
{ Name: 'sub', Value: 'b30498c2-d0f1-70a8-4b0c-3da25a3b998f' },
{ Name: 'family_name', Value: 'New FamilyName 2' },
],
UserCreateDate: 1744206331.569,
UserLastModifiedDate: 1744206366.034,
Enabled: true,
UserStatus: 'FORCE_CHANGE_PASSWORD',
});
});
new NodeTestHarness().setupTests({
workflowFiles: ['get.workflow.json'],
credentials: {
aws: {
region: 'eu-central-1',
accessKeyId: 'test',
secretAccessKey: 'test',
},
},
});
});
@@ -0,0 +1,70 @@
{
"nodes": [
{
"parameters": {},
"id": "4570d7a2-f10a-495d-8a0e-8520b638649e",
"name": "When clicking Execute workflow",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [-780, 380]
},
{
"parameters": {
"operation": "get",
"userPool": {
"__rl": true,
"value": "eu-central-1_EUZ4iEF1T",
"mode": "list",
"cachedResultName": "UserPoolTwo"
},
"user": {
"__rl": true,
"value": "b30498c2-d0f1-70a8-4b0c-3da25a3b998f",
"mode": "list",
"cachedResultName": "userName10"
},
"requestOptions": {}
},
"id": "2105b4ec-0db4-40f5-9d38-78c0fe20708f",
"name": "getUser",
"type": "n8n-nodes-base.awsCognito",
"typeVersion": 1,
"position": [-620, 500],
"alwaysOutputData": true,
"credentials": {
"aws": {
"id": "exampleId",
"name": "AWS account Central Europe"
}
}
}
],
"connections": {
"When clicking Execute workflow": {
"main": [
[
{
"node": "getUser",
"type": "main",
"index": 0
}
]
]
}
},
"pinData": {
"getUser": [
{
"json": {
"Enabled": true,
"UserCreateDate": 1744206331.569,
"UserLastModifiedDate": 1744206366.034,
"UserStatus": "FORCE_CHANGE_PASSWORD",
"Username": "userName10",
"family_name": "New FamilyName 2",
"sub": "b30498c2-d0f1-70a8-4b0c-3da25a3b998f"
}
}
]
}
}
@@ -0,0 +1,64 @@
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
import nock from 'nock';
describe('AWS Cognito - Get All Users', () => {
beforeEach(() => {
const baseUrl = 'https://cognito-idp.eu-central-1.amazonaws.com/';
nock(baseUrl)
.persist()
.defaultReplyHeaders({ 'Content-Type': 'application/x-amz-json-1.1' })
.post('/', {
UserPoolId: 'eu-central-1_KkXQgdCJv',
})
.matchHeader('x-amz-target', 'AWSCognitoIdentityProviderService.ListUsers')
.reply(200, {
Users: [
{
Username: '034448d2-4011-7079-9474-9a4fccd4247a',
Attributes: [
{ Name: 'email', Value: 'FinalUser@gmail.com' },
{ Name: 'Sub', Value: '034448d2-4011-7079-9474-9a4fccd4247a' },
{ Name: 'Enabled', Value: true },
{ Name: 'UserCreateDate', Value: 1736343033.226 },
{ Name: 'UserLastModifiedDate', Value: 1736343033.226 },
{ Name: 'UserStatus', Value: 'FORCE_CHANGE_PASSWORD' },
],
},
{
Username: '03a438f2-10d1-70f1-f45a-09753ab5c4c3',
Attributes: [
{ Name: 'email', Value: 'mail.this1@gmail.com' },
{ Name: 'Sub', Value: '03a438f2-10d1-70f1-f45a-09753ab5c4c3' },
{ Name: 'Enabled', Value: true },
{ Name: 'UserCreateDate', Value: 1733746687.223 },
{ Name: 'UserLastModifiedDate', Value: 1733746687.223 },
{ Name: 'UserStatus', Value: 'FORCE_CHANGE_PASSWORD' },
],
},
{
Username: '03f438d2-b0f1-70bc-04d9-f6dd31f2d878',
Attributes: [
{ Name: 'email', Value: 'test3@gmail.com' },
{ Name: 'Sub', Value: '03f438d2-b0f1-70bc-04d9-f6dd31f2d878' },
{ Name: 'Enabled', Value: true },
{ Name: 'UserCreateDate', Value: 1742928785.796 },
{ Name: 'UserLastModifiedDate', Value: 1742928785.796 },
{ Name: 'UserStatus', Value: 'FORCE_CHANGE_PASSWORD' },
],
},
],
});
});
new NodeTestHarness().setupTests({
workflowFiles: ['getAll.workflow.json'],
credentials: {
aws: {
region: 'eu-central-1',
accessKeyId: 'test',
secretAccessKey: 'test',
},
},
});
});
@@ -0,0 +1,86 @@
{
"nodes": [
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [260, 360],
"id": "9ed2b86b-7c24-4ea0-a328-92d9e6dba35a",
"name": "When clicking Execute workflow"
},
{
"parameters": {
"userPool": {
"__rl": true,
"value": "eu-central-1_KkXQgdCJv",
"mode": "list",
"cachedResultName": "AWS test"
},
"returnAll": true,
"requestOptions": {}
},
"id": "e788623f-5d4a-4e76-b7f6-3e9389e2fe29",
"name": "getAllUsers",
"type": "n8n-nodes-base.awsCognito",
"typeVersion": 1,
"position": [460, 360],
"alwaysOutputData": true,
"credentials": {
"aws": {
"id": "testId",
"name": "AWS account Central Europe"
}
}
}
],
"pinData": {
"getAllUsers": [
{
"json": {
"Enabled": true,
"UserCreateDate": 1736343033.226,
"UserLastModifiedDate": 1736343033.226,
"UserStatus": "FORCE_CHANGE_PASSWORD",
"Username": "034448d2-4011-7079-9474-9a4fccd4247a",
"email": "FinalUser@gmail.com",
"Sub": "034448d2-4011-7079-9474-9a4fccd4247a"
}
},
{
"json": {
"Enabled": true,
"UserCreateDate": 1733746687.223,
"UserLastModifiedDate": 1733746687.223,
"UserStatus": "FORCE_CHANGE_PASSWORD",
"Username": "03a438f2-10d1-70f1-f45a-09753ab5c4c3",
"email": "mail.this1@gmail.com",
"Sub": "03a438f2-10d1-70f1-f45a-09753ab5c4c3"
}
},
{
"json": {
"Enabled": true,
"UserCreateDate": 1742928785.796,
"UserLastModifiedDate": 1742928785.796,
"UserStatus": "FORCE_CHANGE_PASSWORD",
"Username": "03f438d2-b0f1-70bc-04d9-f6dd31f2d878",
"email": "test3@gmail.com",
"Sub": "03f438d2-b0f1-70bc-04d9-f6dd31f2d878"
}
}
]
},
"connections": {
"When clicking Execute workflow": {
"main": [
[
{
"node": "getAllUsers",
"type": "main",
"index": 0
}
]
]
}
}
}
@@ -0,0 +1,74 @@
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
import nock from 'nock';
describe('AWS Cognito - Remove User From Group', () => {
beforeEach(() => {
const baseUrl = 'https://cognito-idp.eu-central-1.amazonaws.com/';
nock(baseUrl)
.persist()
.defaultReplyHeaders({ 'Content-Type': 'application/x-amz-json-1.1' })
.post('/', {
UserPoolId: 'eu-central-1_W3WwpiBXV',
})
.matchHeader('x-amz-target', 'AWSCognitoIdentityProviderService.DescribeUserPool')
.reply(200, {
UserPool: {
Arn: 'arn:aws:cognito-idp:eu-central-1:130450532146:userpool/eu-central-1_W3WwpiBXV',
CreationDate: 1739530218.869,
DeletionProtection: 'INACTIVE',
EstimatedNumberOfUsers: 4,
Id: 'eu-central-1_W3WwpiBXV',
LastModifiedDate: 1739530218.869,
MfaConfiguration: 'OFF',
Name: 'UserPoolSimple',
},
});
nock(baseUrl)
.persist()
.defaultReplyHeaders({ 'Content-Type': 'application/x-amz-json-1.1' })
.post('/', {
UserPoolId: 'eu-central-1_W3WwpiBXV',
Filter: 'sub = "0394e8e2-5081-7020-06bd-44bdfc84dd10"',
})
.matchHeader('x-amz-target', 'AWSCognitoIdentityProviderService.ListUsers')
.reply(200, {
Users: [
{
Username: '0394e8e2-5081-7020-06bd-44bdfc84dd10',
Attributes: [
{ Name: 'email', Value: 'UserSimple' },
{ Name: 'Sub', Value: '0394e8e2-5081-7020-06bd-44bdfc84dd10' },
{ Name: 'Enabled', Value: true },
{ Name: 'UserCreateDate', Value: 1736343033.226 },
{ Name: 'UserLastModifiedDate', Value: 1736343033.226 },
{ Name: 'UserStatus', Value: 'FORCE_CHANGE_PASSWORD' },
],
},
],
});
nock(baseUrl)
.persist()
.defaultReplyHeaders({ 'Content-Type': 'application/x-amz-json-1.1' })
.post('/', {
UserPoolId: 'eu-central-1_W3WwpiBXV',
Username: '0394e8e2-5081-7020-06bd-44bdfc84dd10',
GroupName: 'MyNewGroupSimple',
})
.matchHeader('x-amz-target', 'AWSCognitoIdentityProviderService.AdminRemoveUserFromGroup')
.reply(200, {});
});
new NodeTestHarness().setupTests({
workflowFiles: ['removeFromGroup.workflow.json'],
credentials: {
aws: {
region: 'eu-central-1',
accessKeyId: 'test',
secretAccessKey: 'test',
},
},
});
});
@@ -0,0 +1,69 @@
{
"nodes": [
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [-80, -100],
"id": "7da2ce49-9a9d-4240-b082-ff1b12d101b1",
"name": "When clicking Execute workflow"
},
{
"parameters": {
"operation": "removeFromGroup",
"userPool": {
"__rl": true,
"value": "eu-central-1_W3WwpiBXV",
"mode": "list",
"cachedResultName": "UserPoolSimple"
},
"user": {
"__rl": true,
"value": "0394e8e2-5081-7020-06bd-44bdfc84dd10",
"mode": "list",
"cachedResultName": "UserSimple"
},
"group": {
"__rl": true,
"value": "MyNewGroupSimple",
"mode": "list",
"cachedResultName": "MyNewGroupSimple"
},
"requestOptions": {}
},
"type": "n8n-nodes-base.awsCognito",
"typeVersion": 1,
"position": [100, -100],
"id": "0accb395-3cdb-4c3f-8adc-1a47567c37b5",
"name": "AWS Cognito",
"credentials": {
"aws": {
"id": "testId",
"name": "AWS account Central Europe"
}
}
}
],
"connections": {
"When clicking Execute workflow": {
"main": [
[
{
"node": "AWS Cognito",
"type": "main",
"index": 0
}
]
]
}
},
"pinData": {
"AWS Cognito": [
{
"json": {
"removedFromGroup": true
}
}
]
}
}
@@ -0,0 +1,79 @@
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
import nock from 'nock';
describe('AWS Cognito - Update User', () => {
beforeEach(() => {
const baseUrl = 'https://cognito-idp.eu-central-1.amazonaws.com/';
nock(baseUrl)
.persist()
.defaultReplyHeaders({ 'Content-Type': 'application/x-amz-json-1.1' })
.post('/', {
UserPoolId: 'eu-central-1_EUZ4iEF1T',
})
.matchHeader('x-amz-target', 'AWSCognitoIdentityProviderService.DescribeUserPool')
.reply(200, {
UserPool: {
Arn: 'arn:aws:cognito-idp:eu-central-1:130450532146:userpool/eu-central-1_EUZ4iEF1T',
CreationDate: 1739530218.869,
DeletionProtection: 'INACTIVE',
EstimatedNumberOfUsers: 4,
Id: 'eu-central-1_EUZ4iEF1T',
LastModifiedDate: 1739530218.869,
MfaConfiguration: 'OFF',
Name: 'UserPoolTwo',
},
});
nock(baseUrl)
.persist()
.defaultReplyHeaders({ 'Content-Type': 'application/x-amz-json-1.1' })
.post('/', {
UserPoolId: 'eu-central-1_EUZ4iEF1T',
Filter: 'sub = "43045822-80e1-70f6-582d-78ae7992e9d9"',
})
.matchHeader('x-amz-target', 'AWSCognitoIdentityProviderService.ListUsers')
.reply(200, {
Users: [
{
Username: '43045822-80e1-70f6-582d-78ae7992e9d9',
Attributes: [
{ Name: 'email', Value: 'UserSimple' },
{ Name: 'Sub', Value: '43045822-80e1-70f6-582d-78ae7992e9d9' },
{ Name: 'Enabled', Value: true },
{ Name: 'UserCreateDate', Value: 1736343033.226 },
{ Name: 'UserLastModifiedDate', Value: 1736343033.226 },
{ Name: 'UserStatus', Value: 'FORCE_CHANGE_PASSWORD' },
],
},
],
});
nock(baseUrl)
.persist()
.defaultReplyHeaders({ 'Content-Type': 'application/x-amz-json-1.1' })
.post('/', {
UserPoolId: 'eu-central-1_EUZ4iEF1T',
Username: '43045822-80e1-70f6-582d-78ae7992e9d9',
UserAttributes: [
{
Name: 'address',
Value: 'New',
},
],
})
.matchHeader('x-amz-target', 'AWSCognitoIdentityProviderService.AdminUpdateUserAttributes')
.reply(200, {});
});
new NodeTestHarness().setupTests({
workflowFiles: ['update.workflow.json'],
credentials: {
aws: {
region: 'eu-central-1',
accessKeyId: 'test',
secretAccessKey: 'test',
},
},
});
});
@@ -0,0 +1,71 @@
{
"nodes": [
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [-80, -100],
"id": "7da2ce49-9a9d-4240-b082-ff1b12d101b1",
"name": "When clicking Execute workflow"
},
{
"parameters": {
"operation": "update",
"userPool": {
"__rl": true,
"value": "eu-central-1_EUZ4iEF1T",
"mode": "list",
"cachedResultName": "UserPoolTwo"
},
"user": {
"__rl": true,
"value": "43045822-80e1-70f6-582d-78ae7992e9d9",
"mode": "list",
"cachedResultName": "Johnn"
},
"userAttributes": {
"attributes": [
{
"standardName": "address",
"value": "New"
}
]
},
"requestOptions": {}
},
"type": "n8n-nodes-base.awsCognito",
"typeVersion": 1,
"position": [120, -100],
"id": "4c6bdeea-f863-4974-b3d6-7dfc04f78df3",
"name": "AWS Cognito",
"credentials": {
"aws": {
"id": "exampleId",
"name": "AWS account Central Europe"
}
}
}
],
"connections": {
"When clicking Execute workflow": {
"main": [
[
{
"node": "AWS Cognito",
"type": "main",
"index": 0
}
]
]
}
},
"pinData": {
"AWS Cognito": [
{
"json": {
"updated": true
}
}
]
}
}
@@ -0,0 +1,83 @@
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
import nock from 'nock';
describe('AWS Cognito - Get User Pool', () => {
beforeEach(() => {
const baseUrl = 'https://cognito-idp.eu-central-1.amazonaws.com/';
nock(baseUrl)
.persist()
.defaultReplyHeaders({ 'Content-Type': 'application/x-amz-json-1.1' })
.post('/', {
UserPoolId: 'eu-central-1_W3WwpiBXV',
})
.matchHeader('x-amz-target', 'AWSCognitoIdentityProviderService.DescribeUserPool')
.reply(200, {
UserPool: {
Arn: 'arn:aws:cognito-idp:eu-central-1:130450532146:userpool/eu-central-1_W3WwpiBXV',
CreationDate: 1739527771.267,
DeletionProtection: 'INACTIVE',
EstimatedNumberOfUsers: 8,
Id: 'eu-central-1_W3WwpiBXV',
LastModifiedDate: 1739527771.267,
MfaConfiguration: 'OFF',
Name: 'UserPoolSimple',
},
});
nock(baseUrl)
.persist()
.defaultReplyHeaders({ 'Content-Type': 'application/x-amz-json-1.1' })
.post('/', {
UserPoolId: 'eu-central-1_EUZ4iEF1T',
Limit: 50,
})
.matchHeader('x-amz-target', 'AWSCognitoIdentityProviderService.ListUsers')
.reply(200, {
Users: [
{
Username: 'userName10',
UserAttributes: [
{ Name: 'sub', Value: 'b30498c2-d0f1-70a8-4b0c-3da25a3b998f' },
{ Name: 'family_name', Value: 'New FamilyName 2' },
],
UserCreateDate: 1744206331.569,
UserLastModifiedDate: 1744206366.034,
Enabled: true,
UserStatus: 'FORCE_CHANGE_PASSWORD',
},
],
});
nock(baseUrl)
.persist()
.defaultReplyHeaders({ 'Content-Type': 'application/x-amz-json-1.1' })
.post('/', {
UserPoolId: 'eu-central-1_EUZ4iEF1T',
Username: 'b30498c2-d0f1-70a8-4b0c-3da25a3b998f',
})
.matchHeader('x-amz-target', 'AWSCognitoIdentityProviderService.AdminGetUser')
.reply(200, {
Username: 'userName10',
UserAttributes: [
{ Name: 'sub', Value: 'b30498c2-d0f1-70a8-4b0c-3da25a3b998f' },
{ Name: 'family_name', Value: 'New FamilyName 2' },
],
UserCreateDate: 1744206331.569,
UserLastModifiedDate: 1744206366.034,
Enabled: true,
UserStatus: 'FORCE_CHANGE_PASSWORD',
});
});
new NodeTestHarness().setupTests({
workflowFiles: ['get.workflow.json'],
credentials: {
aws: {
region: 'eu-central-1',
accessKeyId: 'test',
secretAccessKey: 'test',
},
},
});
});
@@ -0,0 +1,64 @@
{
"nodes": [
{
"parameters": {},
"id": "4570d7a2-f10a-495d-8a0e-8520b638649e",
"name": "When clicking Execute workflow",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [-700, 320]
},
{
"parameters": {
"resource": "userPool",
"userPool": {
"__rl": true,
"value": "eu-central-1_W3WwpiBXV",
"mode": "list",
"cachedResultName": "UserPoolSimple"
},
"requestOptions": {}
},
"type": "n8n-nodes-base.awsCognito",
"typeVersion": 1,
"position": [-700, 500],
"id": "fc734bb9-acd3-4b7d-a2a3-0a7b664e3c5a",
"name": "AWS Cognito2",
"credentials": {
"aws": {
"id": "testId",
"name": "AWS account Central Europe"
}
}
}
],
"connections": {
"When clicking Execute workflow": {
"main": [
[
{
"node": "AWS Cognito2",
"type": "main",
"index": 0
}
]
]
}
},
"pinData": {
"AWS Cognito2": [
{
"json": {
"Arn": "arn:aws:cognito-idp:eu-central-1:130450532146:userpool/eu-central-1_W3WwpiBXV",
"CreationDate": 1739527771.267,
"DeletionProtection": "INACTIVE",
"EstimatedNumberOfUsers": 8,
"Id": "eu-central-1_W3WwpiBXV",
"LastModifiedDate": 1739527771.267,
"MfaConfiguration": "OFF",
"Name": "UserPoolSimple"
}
}
]
}
}
@@ -0,0 +1,71 @@
import type {
ILoadOptionsFunctions,
IPollFunctions,
IHttpRequestOptions,
IExecuteSingleFunctions,
IDataObject,
IHttpRequestMethods,
} from 'n8n-workflow';
import type { AwsIamCredentialsType } from '../../../../credentials/common/aws/types';
export async function awsApiRequest(
this: ILoadOptionsFunctions | IPollFunctions | IExecuteSingleFunctions,
method: IHttpRequestMethods,
action: string,
body: string,
): Promise<any> {
const credentialsType = 'aws';
const credentials = await this.getCredentials<AwsIamCredentialsType>(credentialsType);
const requestOptions: IHttpRequestOptions = {
url: '',
method,
body,
headers: {
'Content-Type': 'application/x-amz-json-1.1',
'X-Amz-Target': `AWSCognitoIdentityProviderService.${action}`,
},
qs: {
service: 'cognito-idp',
_region: credentials.region,
},
};
return await this.helpers.httpRequestWithAuthentication.call(
this,
credentialsType,
requestOptions,
);
}
export async function awsApiRequestAllItems(
this: ILoadOptionsFunctions | IPollFunctions | IExecuteSingleFunctions,
method: IHttpRequestMethods,
action: string,
body: IDataObject,
propertyName: string,
): Promise<IDataObject[]> {
const returnData: IDataObject[] = [];
let nextToken: string | undefined;
do {
const requestBody: IDataObject = {
...body,
...(nextToken ? { NextToken: nextToken } : {}),
};
const response = (await awsApiRequest.call(
this,
method,
action,
JSON.stringify(requestBody),
)) as IDataObject;
const items = (response[propertyName] ?? []) as IDataObject[];
returnData.push(...items);
nextToken = response.NextToken as string | undefined;
} while (nextToken);
return returnData;
}
@@ -0,0 +1,24 @@
{
"node": "n8n-nodes-base.awsComprehend",
"nodeVersion": "1.0",
"codexVersion": "1.0",
"categories": ["Development"],
"resources": {
"credentialDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/credentials/aws/"
}
],
"primaryDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.awscomprehend/"
}
],
"generic": [
{
"label": "7 no-code workflow automations for Amazon Web Services",
"url": "https://n8n.io/blog/aws-workflow-automation/"
}
]
}
}
@@ -0,0 +1,291 @@
import type {
IExecuteFunctions,
IDataObject,
INodeExecutionData,
INodeType,
INodeTypeDescription,
} from 'n8n-workflow';
import { NodeConnectionTypes } from 'n8n-workflow';
import { awsApiRequestREST } from './GenericFunctions';
import { awsNodeAuthOptions, awsNodeCredentials } from '../utils';
export class AwsComprehend implements INodeType {
description: INodeTypeDescription = {
displayName: 'AWS Comprehend',
name: 'awsComprehend',
icon: 'file:comprehend.svg',
group: ['output'],
version: 1,
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Sends data to Amazon Comprehend',
schemaPath: 'Aws/Comprehend',
defaults: {
name: 'AWS Comprehend',
},
inputs: [NodeConnectionTypes.Main],
outputs: [NodeConnectionTypes.Main],
credentials: awsNodeCredentials,
properties: [
awsNodeAuthOptions,
{
displayName: 'Resource',
name: 'resource',
type: 'options',
noDataExpression: true,
options: [
{
name: 'Text',
value: 'text',
},
],
default: 'text',
description: 'The resource to perform',
},
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
options: [
{
name: 'Detect Dominant Language',
value: 'detectDominantLanguage',
description: 'Identify the dominant language',
action: 'Identify the dominant language',
},
{
name: 'Detect Entities',
value: 'detectEntities',
description: 'Inspects text for named entities, and returns information about them',
action: 'Inspect text for named entities, and returns information about them',
},
{
name: 'Detect Sentiment',
value: 'detectSentiment',
description: 'Analyse the sentiment of the text',
action: 'Analyze the sentiment of the text',
},
],
default: 'detectDominantLanguage',
},
{
displayName: 'Language Code',
name: 'languageCode',
type: 'options',
options: [
{
name: 'Arabic',
value: 'ar',
},
{
name: 'Chinese',
value: 'zh',
},
{
name: 'Chinese (T)',
value: 'zh-TW',
},
{
name: 'English',
value: 'en',
},
{
name: 'French',
value: 'fr',
},
{
name: 'German',
value: 'de',
},
{
name: 'Hindi',
value: 'hi',
},
{
name: 'Italian',
value: 'it',
},
{
name: 'Japanese',
value: 'ja',
},
{
name: 'Korean',
value: 'ko',
},
{
name: 'Portuguese',
value: 'pt',
},
{
name: 'Spanish',
value: 'es',
},
],
default: 'en',
displayOptions: {
show: {
resource: ['text'],
operation: ['detectSentiment', 'detectEntities'],
},
},
description: 'The language code for text',
},
{
displayName: 'Text',
name: 'text',
type: 'string',
default: '',
displayOptions: {
show: {
resource: ['text'],
},
},
description: 'The text to send',
},
{
displayName: 'Simplify',
name: 'simple',
type: 'boolean',
displayOptions: {
show: {
resource: ['text'],
operation: ['detectDominantLanguage'],
},
},
default: true,
description:
'Whether to return a simplified version of the response instead of the raw data',
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
displayOptions: {
show: {
resource: ['text'],
operation: ['detectEntities'],
},
},
default: {},
options: [
{
displayName: 'Endpoint Arn',
name: 'endpointArn',
type: 'string',
default: '',
description:
'The Amazon Resource Name of an endpoint that is associated with a custom entity recognition model',
},
],
},
],
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: INodeExecutionData[] = [];
let responseData;
const resource = this.getNodeParameter('resource', 0);
const operation = this.getNodeParameter('operation', 0);
for (let i = 0; i < items.length; i++) {
try {
if (resource === 'text') {
//https://docs.aws.amazon.com/comprehend/latest/dg/API_DetectDominantLanguage.html
if (operation === 'detectDominantLanguage') {
const text = this.getNodeParameter('text', i) as string;
const simple = this.getNodeParameter('simple', i) as boolean;
const body: IDataObject = {
Text: text,
};
const action = 'Comprehend_20171127.DetectDominantLanguage';
responseData = await awsApiRequestREST.call(
this,
'comprehend',
'POST',
'',
JSON.stringify(body),
{ 'x-amz-target': action, 'Content-Type': 'application/x-amz-json-1.1' },
);
if (simple) {
responseData = responseData.Languages.reduce(
(accumulator: { [key: string]: number }, currentValue: IDataObject) => {
accumulator[currentValue.LanguageCode as string] = currentValue.Score as number;
return accumulator;
},
{},
);
}
}
//https://docs.aws.amazon.com/comprehend/latest/dg/API_DetectSentiment.html
if (operation === 'detectSentiment') {
const action = 'Comprehend_20171127.DetectSentiment';
const text = this.getNodeParameter('text', i) as string;
const languageCode = this.getNodeParameter('languageCode', i) as string;
const body: IDataObject = {
Text: text,
LanguageCode: languageCode,
};
responseData = await awsApiRequestREST.call(
this,
'comprehend',
'POST',
'',
JSON.stringify(body),
{ 'x-amz-target': action, 'Content-Type': 'application/x-amz-json-1.1' },
);
}
//https://docs.aws.amazon.com/comprehend/latest/dg/API_DetectEntities.html
if (operation === 'detectEntities') {
const action = 'Comprehend_20171127.DetectEntities';
const text = this.getNodeParameter('text', i) as string;
const languageCode = this.getNodeParameter('languageCode', i) as string;
const additionalFields = this.getNodeParameter('additionalFields', i);
const body: IDataObject = {
Text: text,
LanguageCode: languageCode,
};
if (additionalFields.endpointArn) {
body.EndpointArn = additionalFields.endpointArn;
}
responseData = await awsApiRequestREST.call(
this,
'comprehend',
'POST',
'',
JSON.stringify(body),
{ 'x-amz-target': action, 'Content-Type': 'application/x-amz-json-1.1' },
);
responseData = responseData.Entities;
}
}
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(
this.helpers.returnJsonArray({ error: error.message }),
{ itemData: { item: i } },
);
returnData.push(...executionData);
continue;
}
throw error;
}
}
return [returnData];
}
}
@@ -0,0 +1,73 @@
import type {
IExecuteFunctions,
IHookFunctions,
ILoadOptionsFunctions,
IWebhookFunctions,
IHttpRequestOptions,
IHttpRequestMethods,
} from 'n8n-workflow';
import { parseString } from 'xml2js';
import { getAwsCredentials } from '../GenericFunctions';
export async function awsApiRequest(
this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions | IWebhookFunctions,
service: string,
method: IHttpRequestMethods,
path: string,
body?: string,
headers?: object,
): Promise<any> {
const { credentials, credentialsType } = await getAwsCredentials(this);
const requestOptions = {
qs: {
service,
path,
},
method,
body,
url: '',
headers,
region: credentials?.region as string,
} as IHttpRequestOptions;
return await this.helpers.requestWithAuthentication.call(this, credentialsType, requestOptions);
}
export async function awsApiRequestREST(
this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions,
service: string,
method: IHttpRequestMethods,
path: string,
body?: string,
headers?: object,
): Promise<any> {
const response = await awsApiRequest.call(this, service, method, path, body, headers);
try {
return JSON.parse(response as string);
} catch (error) {
return response;
}
}
export async function awsApiRequestSOAP(
this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions | IWebhookFunctions,
service: string,
method: IHttpRequestMethods,
path: string,
body?: string,
headers?: object,
): Promise<any> {
const response = await awsApiRequest.call(this, service, method, path, body, headers);
try {
return await new Promise((resolve, reject) => {
parseString(response as string, { explicitArray: false }, (err, data) => {
if (err) {
return reject(err);
}
resolve(data);
});
});
} catch (error) {
return response;
}
}
@@ -0,0 +1,21 @@
{
"type": "object",
"properties": {
"BeginOffset": {
"type": "integer"
},
"EndOffset": {
"type": "integer"
},
"Score": {
"type": "number"
},
"Text": {
"type": "string"
},
"Type": {
"type": "string"
}
},
"version": 1
}
@@ -0,0 +1,26 @@
{
"type": "object",
"properties": {
"Sentiment": {
"type": "string"
},
"SentimentScore": {
"type": "object",
"properties": {
"Mixed": {
"type": "number"
},
"Negative": {
"type": "number"
},
"Neutral": {
"type": "number"
},
"Positive": {
"type": "number"
}
}
}
},
"version": 1
}
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 75 75"><defs><linearGradient id="a" x1="617.46" x2="723.53" y1="-674.53" y2="-568.46" gradientTransform="rotate(-90 683.5 24.5)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#055f4e"/><stop offset="1" stop-color="#56c0a7"/></linearGradient></defs><path fill="url(#a)" d="M0 0h75v75H0z" data-name="Turquoise Gradient"/><path fill="#fff" d="M44.5 34.2v-9.7a1 1 0 0 0-.29-.71l-11-11a1 1 0 0 0-.71-.29h-19a1 1 0 0 0-1 1v43a1 1 0 0 0 1 1h30a1 1 0 0 0 1-1v-4.44a11.8 11.8 0 0 1-2-2.3v5.74h-28v-41h17v10a1 1 0 0 0 1 1h10v11a11.6 11.6 0 0 1 2-2.3m-11-10.7v-7.59l7.59 7.59zm-10 8h-6v-2h6zm16 0h-14v-2h14zm0 6h-22v-2h22zm15.44 25h-4.88a1 1 0 0 1-.93-.62l-1.21-3a1 1 0 0 1 .09-.94 1 1 0 0 1 .83-.44h7.32a1 1 0 0 1 .83.44 1 1 0 0 1 .09.94l-1.21 3a1 1 0 0 1-.93.62m-4.21-2h3.54l.4-1h-4.34zm11.64-19a10 10 0 0 0-19.87 1.62 10 10 0 0 0 4.28 8.2 4 4 0 0 1 .72.59v3.59a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-3.6a4.3 4.3 0 0 1 .71-.57 9.92 9.92 0 0 0 4.29-8.2 10 10 0 0 0-.13-1.65zm-5.32 8.2c-.58.4-1.55 1.07-1.55 2.1v2.7h-2v-7h2v-2h-6v2h2v7h-2v-2.68c0-1-1-1.73-1.58-2.14A8 8 0 1 1 58 37.32a7.9 7.9 0 0 1 2.39 4.47 8 8 0 0 1-3.34 7.91M28.5 25.5h-11v-2h11zm1 18h-12v-2h12zm10 0h-8v-2h8zm-9 6h-13v-2h13z" data-name="Icon Test"/></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

@@ -0,0 +1,37 @@
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
import nock from 'nock';
import { credentials } from '../../__tests__/credentials';
describe('Test AWS Comprehend Node', () => {
describe('Detect Language', () => {
let mock: nock.Scope;
const now = 1683028800000;
const response = {
Languages: [
{
LanguageCode: 'en',
Score: 0.9774383902549744,
},
{
LanguageCode: 'de',
Score: 0.010717987082898617,
},
],
};
beforeAll(async () => {
jest.useFakeTimers({ doNotFake: ['nextTick'], now });
const baseUrl = 'https://comprehend.eu-central-1.amazonaws.com';
mock = nock(baseUrl);
});
beforeEach(async () => {
mock.post('/').reply(200, response);
});
new NodeTestHarness().setupTests({ credentials });
});
});
@@ -0,0 +1,113 @@
{
"name": "node-aws-comprehend",
"nodes": [
{
"parameters": {},
"id": "53b6020d-5aa2-435f-9ee1-407111c0e3ee",
"name": "When clicking Execute workflow",
"type": "n8n-nodes-base.manualTrigger",
"position": [680, 380],
"typeVersion": 1
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "51b7eee5-8cc9-4e09-a2f4-65ffb3cc17f6",
"name": "text",
"value": "This is a test.",
"type": "string"
}
]
},
"options": {}
},
"id": "b3beaf43-fe4c-43e1-a8cb-5a0740050611",
"name": "Edit Fields",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [900, 380]
},
{
"parameters": {
"text": "={{ $json.text }}"
},
"id": "a6a8a24c-0e58-40e7-8bf4-13a56edc6264",
"name": "AWS Comprehend",
"type": "n8n-nodes-base.awsComprehend",
"typeVersion": 1,
"position": [1100, 380],
"credentials": {
"aws": {
"id": "TyNATsPCTvPF0tvG",
"name": "AWS account"
}
}
},
{
"parameters": {},
"id": "bfc4b84d-8cf1-4650-bf3c-2b1cdc677afc",
"name": "No Operation, do nothing",
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [1320, 380]
}
],
"pinData": {
"No Operation, do nothing": [
{
"json": {
"de": 0.010717987082898617,
"en": 0.9774383902549744
}
}
]
},
"connections": {
"Edit Fields": {
"main": [
[
{
"node": "AWS Comprehend",
"type": "main",
"index": 0
}
]
]
},
"When clicking Execute workflow": {
"main": [
[
{
"node": "Edit Fields",
"type": "main",
"index": 0
}
]
]
},
"AWS Comprehend": {
"main": [
[
{
"node": "No Operation, do nothing",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1"
},
"versionId": "eae3c601-56b8-42ec-a0b7-14df8d697043",
"meta": {
"templateCredsSetupCompleted": true,
"instanceId": "be251a83c052a9862eeac953816fbb1464f89dfbf79d7ac490a8e336a8cc8bfd"
},
"id": "fuOmKcLPWAxKi0bn",
"tags": []
}
@@ -0,0 +1,18 @@
{
"node": "n8n-nodes-base.awsDynamoDb",
"nodeVersion": "1.0",
"codexVersion": "1.0",
"categories": ["Data & Storage", "Development"],
"resources": {
"credentialDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/credentials/aws/"
}
],
"primaryDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.awsdynamodb/"
}
]
}
}
@@ -0,0 +1,412 @@
/* eslint-disable n8n-nodes-base/node-filename-against-convention */
import {
type IExecuteFunctions,
type IDataObject,
type ILoadOptionsFunctions,
type INodeExecutionData,
type INodeType,
type INodeTypeDescription,
type NodeParameterValue,
NodeConnectionTypes,
} from 'n8n-workflow';
import { awsApiRequest, awsApiRequestAllItems } from './GenericFunctions';
import { itemFields, itemOperations } from './ItemDescription';
import type {
FieldsUiValues,
IAttributeNameUi,
IAttributeValue,
IAttributeValueUi,
IRequestBody,
PutItemUi,
} from './types';
import {
adjustExpressionAttributeName,
adjustExpressionAttributeValues,
adjustPutItem,
decodeItem,
simplify,
} from './utils';
import { awsNodeAuthOptions, awsNodeCredentials } from '../utils';
export class AwsDynamoDB implements INodeType {
description: INodeTypeDescription = {
displayName: 'AWS DynamoDB',
name: 'awsDynamoDb',
icon: 'file:dynamodb.svg',
group: ['transform'],
version: 1,
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Consume the AWS DynamoDB API',
schemaPath: 'Aws/DynamoDB',
defaults: {
name: 'AWS DynamoDB',
},
inputs: [NodeConnectionTypes.Main],
outputs: [NodeConnectionTypes.Main],
credentials: awsNodeCredentials,
properties: [
awsNodeAuthOptions,
{
displayName: 'Resource',
name: 'resource',
type: 'options',
noDataExpression: true,
options: [
{
name: 'Item',
value: 'item',
},
],
default: 'item',
},
...itemOperations,
...itemFields,
],
};
methods = {
loadOptions: {
async getTables(this: ILoadOptionsFunctions) {
const headers = {
'Content-Type': 'application/x-amz-json-1.0',
'X-Amz-Target': 'DynamoDB_20120810.ListTables',
};
const responseData = await awsApiRequest.call(this, 'dynamodb', 'POST', '/', {}, headers);
return responseData.TableNames.map((table: string) => ({ name: table, value: table }));
},
},
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const resource = this.getNodeParameter('resource', 0);
const operation = this.getNodeParameter('operation', 0);
let responseData;
const returnData: INodeExecutionData[] = [];
for (let i = 0; i < items.length; i++) {
try {
if (resource === 'item') {
if (operation === 'upsert') {
// ----------------------------------
// upsert
// ----------------------------------
// https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_PutItem.html
const eavUi = this.getNodeParameter(
'additionalFields.eavUi.eavValues',
i,
[],
) as IAttributeValueUi[];
const conditionExpession = this.getNodeParameter(
'additionalFields.conditionExpression',
i,
'',
) as string;
const eanUi = this.getNodeParameter(
'additionalFields.eanUi.eanValues',
i,
[],
) as IAttributeNameUi[];
const body: IRequestBody = {
TableName: this.getNodeParameter('tableName', i) as string,
};
const expressionAttributeValues = adjustExpressionAttributeValues(eavUi);
if (Object.keys(expressionAttributeValues).length) {
body.ExpressionAttributeValues = expressionAttributeValues;
}
const expressionAttributeName = adjustExpressionAttributeName(eanUi);
if (Object.keys(expressionAttributeName).length) {
body.ExpressionAttributeNames = expressionAttributeName;
}
if (conditionExpession) {
body.ConditionExpression = conditionExpession;
}
const dataToSend = this.getNodeParameter('dataToSend', 0) as
| 'defineBelow'
| 'autoMapInputData';
const item: { [key: string]: string } = {};
if (dataToSend === 'autoMapInputData') {
const incomingKeys = Object.keys(items[i].json);
const rawInputsToIgnore = this.getNodeParameter('inputsToIgnore', i) as string;
const inputsToIgnore = rawInputsToIgnore.split(',').map((c) => c.trim());
for (const key of incomingKeys) {
if (inputsToIgnore.includes(key)) continue;
item[key] = items[i].json[key] as string;
}
body.Item = adjustPutItem(item as PutItemUi);
} else {
const fields = this.getNodeParameter('fieldsUi.fieldValues', i, []) as FieldsUiValues;
fields.forEach(({ fieldId, fieldValue }) => (item[fieldId] = fieldValue));
body.Item = adjustPutItem(item as PutItemUi);
}
const headers = {
'Content-Type': 'application/x-amz-json-1.0',
'X-Amz-Target': 'DynamoDB_20120810.PutItem',
};
responseData = await awsApiRequest.call(this, 'dynamodb', 'POST', '/', body, headers);
responseData = item;
} else if (operation === 'delete') {
// ----------------------------------
// delete
// ----------------------------------
// https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_DeleteItem.html
const body: { [key: string]: any } = {
TableName: this.getNodeParameter('tableName', i) as string,
Key: {},
ReturnValues: this.getNodeParameter('returnValues', 0) as string,
};
const eavUi = this.getNodeParameter(
'additionalFields.expressionAttributeUi.expressionAttributeValues',
i,
[],
) as IAttributeValueUi[];
const eanUi = this.getNodeParameter(
'additionalFields.eanUi.eanValues',
i,
[],
) as IAttributeNameUi[];
const additionalFields = this.getNodeParameter('additionalFields', i);
const simple = this.getNodeParameter('simple', 0, false) as boolean;
const keyValues = this.getNodeParameter('keysUi.keyValues', i, []) as [
{ key: string; type: string; value: string },
];
for (const item of keyValues) {
let value = item.value as NodeParameterValue;
// All data has to get send as string even numbers
// @ts-ignore
value = ![null, undefined].includes(value) ? value?.toString() : '';
body.Key[item.key] = { [item.type]: value };
}
const expressionAttributeValues = adjustExpressionAttributeValues(eavUi);
if (Object.keys(expressionAttributeValues).length) {
body.ExpressionAttributeValues = expressionAttributeValues;
}
const expressionAttributeName = adjustExpressionAttributeName(eanUi);
if (Object.keys(expressionAttributeName).length) {
body.ExpressionAttributeNames = expressionAttributeName;
}
const headers = {
'Content-Type': 'application/x-amz-json-1.0',
'X-Amz-Target': 'DynamoDB_20120810.DeleteItem',
};
if (additionalFields.conditionExpression) {
body.ConditionExpression = additionalFields.conditionExpression as string;
}
responseData = await awsApiRequest.call(this, 'dynamodb', 'POST', '/', body, headers);
if (!Object.keys(responseData as IDataObject).length) {
responseData = { success: true };
} else if (simple) {
responseData = decodeItem(responseData.Attributes as IAttributeValue);
}
} else if (operation === 'get') {
// ----------------------------------
// get
// ----------------------------------
// https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_GetItem.html
const tableName = this.getNodeParameter('tableName', 0) as string;
const simple = this.getNodeParameter('simple', 0, false) as boolean;
const select = this.getNodeParameter('select', 0) as string;
const additionalFields = this.getNodeParameter('additionalFields', i);
const eanUi = this.getNodeParameter(
'additionalFields.eanUi.eanValues',
i,
[],
) as IAttributeNameUi[];
const body: { [key: string]: any } = {
TableName: tableName,
Key: {},
Select: select,
};
Object.assign(body, additionalFields);
const expressionAttributeName = adjustExpressionAttributeName(eanUi);
if (Object.keys(expressionAttributeName).length) {
body.ExpressionAttributeNames = expressionAttributeName;
}
if (additionalFields.readType) {
body.ConsistentRead = additionalFields.readType === 'stronglyConsistentRead';
}
if (additionalFields.projectionExpression) {
body.ProjectionExpression = additionalFields.projectionExpression as string;
}
const keyValues = this.getNodeParameter('keysUi.keyValues', i, []) as IDataObject[];
for (const item of keyValues) {
let value = item.value as NodeParameterValue;
// All data has to get send as string even numbers
// @ts-ignore
value = ![null, undefined].includes(value) ? value?.toString() : '';
body.Key[item.key as string] = { [item.type as string]: value };
}
const headers = {
'X-Amz-Target': 'DynamoDB_20120810.GetItem',
'Content-Type': 'application/x-amz-json-1.0',
};
responseData = await awsApiRequest.call(this, 'dynamodb', 'POST', '/', body, headers);
responseData = responseData.Item;
if (simple && responseData) {
responseData = decodeItem(responseData as IAttributeValue);
}
} else if (operation === 'getAll') {
// ----------------------------------
// getAll
// ----------------------------------
// https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_Query.html
const eavUi = this.getNodeParameter('eavUi.eavValues', i, []) as IAttributeValueUi[];
const simple = this.getNodeParameter('simple', 0, false) as boolean;
const select = this.getNodeParameter('select', 0) as string;
const returnAll = this.getNodeParameter('returnAll', 0);
const scan = this.getNodeParameter('scan', 0) as boolean;
const eanUi = this.getNodeParameter(
'options.eanUi.eanValues',
i,
[],
) as IAttributeNameUi[];
const body: IRequestBody = {
TableName: this.getNodeParameter('tableName', i) as string,
};
if (scan) {
const filterExpression = this.getNodeParameter('filterExpression', i) as string;
if (filterExpression) {
body.FilterExpression = filterExpression;
}
} else {
body.KeyConditionExpression = this.getNodeParameter(
'keyConditionExpression',
i,
) as string;
}
const { indexName, projectionExpression, filterExpression } = this.getNodeParameter(
'options',
i,
) as {
indexName: string;
projectionExpression: string;
filterExpression: string;
};
const expressionAttributeName = adjustExpressionAttributeName(eanUi);
if (Object.keys(expressionAttributeName).length) {
body.ExpressionAttributeNames = expressionAttributeName;
}
const expressionAttributeValues = adjustExpressionAttributeValues(eavUi);
if (Object.keys(expressionAttributeValues).length) {
body.ExpressionAttributeValues = expressionAttributeValues;
}
if (indexName) {
body.IndexName = indexName;
}
if (projectionExpression && select !== 'COUNT') {
body.ProjectionExpression = projectionExpression;
}
if (filterExpression) {
body.FilterExpression = filterExpression;
}
if (select) {
body.Select = select;
}
const headers = {
'Content-Type': 'application/json',
'X-Amz-Target': scan ? 'DynamoDB_20120810.Scan' : 'DynamoDB_20120810.Query',
};
if (returnAll && select !== 'COUNT') {
responseData = await awsApiRequestAllItems.call(
this,
'dynamodb',
'POST',
'/',
body,
headers,
);
} else {
body.Limit = this.getNodeParameter('limit', 0, 1);
responseData = await awsApiRequest.call(this, 'dynamodb', 'POST', '/', body, headers);
if (select !== 'COUNT') {
responseData = responseData.Items;
}
}
if (simple) {
responseData = responseData.map(simplify);
}
}
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(responseData as IDataObject[]),
{ itemData: { item: i } },
);
returnData.push(...executionData);
}
} catch (error) {
if (this.continueOnFail()) {
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray({ error: error.message }),
{ itemData: { item: i } },
);
returnData.push(...executionData);
continue;
}
throw error;
}
}
return [returnData];
}
}
@@ -0,0 +1,109 @@
import type {
IDataObject,
IExecuteFunctions,
IHookFunctions,
ILoadOptionsFunctions,
IWebhookFunctions,
IHttpRequestOptions,
INodeExecutionData,
IHttpRequestMethods,
} from 'n8n-workflow';
import { ApplicationError, deepCopy } from 'n8n-workflow';
import type { IRequestBody } from './types';
import { getAwsCredentials } from '../GenericFunctions';
export async function awsApiRequest(
this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions | IWebhookFunctions,
service: string,
method: IHttpRequestMethods,
path: string,
body?: object | IRequestBody,
headers?: object,
): Promise<any> {
const { credentials, credentialsType } = await getAwsCredentials(this);
const requestOptions = {
qs: {
service,
path,
},
method,
body: JSON.stringify(body),
url: '',
headers,
region: credentials?.region as string,
} as IHttpRequestOptions;
try {
return JSON.parse(
(await this.helpers.requestWithAuthentication.call(
this,
credentialsType,
requestOptions,
)) as string,
);
} catch (error) {
const statusCode = (error.statusCode || error.cause?.statusCode) as number;
let errorMessage =
error.response?.body?.message || error.response?.body?.Message || error.message;
if (statusCode === 403) {
if (errorMessage === 'The security token included in the request is invalid.') {
throw new ApplicationError('The AWS credentials are not valid!', { level: 'warning' });
} else if (
errorMessage.startsWith(
'The request signature we calculated does not match the signature you provided',
)
) {
throw new ApplicationError('The AWS credentials are not valid!', { level: 'warning' });
}
}
if (error.cause?.error) {
try {
errorMessage = JSON.parse(error.cause?.error).message;
} catch (ex) {}
}
throw new ApplicationError(`AWS error response [${statusCode}]: ${errorMessage}`, {
level: 'warning',
});
}
}
export async function awsApiRequestAllItems(
this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions | IWebhookFunctions,
service: string,
method: IHttpRequestMethods,
path: string,
body?: IRequestBody,
headers?: object,
): Promise<any> {
const returnData: IDataObject[] = [];
let responseData;
do {
const originalHeaders = Object.assign({}, headers); //The awsapirequest function adds the hmac signature to the headers, if we pass the modified headers back in on the next call it will fail with invalid signature
responseData = await awsApiRequest.call(this, service, method, path, body, originalHeaders);
if (responseData.LastEvaluatedKey) {
body!.ExclusiveStartKey = responseData.LastEvaluatedKey;
}
returnData.push(...(responseData.Items as IDataObject[]));
} while (responseData.LastEvaluatedKey !== undefined);
return returnData;
}
export function copyInputItem(item: INodeExecutionData, properties: string[]): IDataObject {
// Prepare the data to insert and copy it to be returned
const newItem: IDataObject = {};
for (const property of properties) {
if (item.json[property] === undefined) {
newItem[property] = null;
} else {
newItem[property] = deepCopy(item.json[property]);
}
}
return newItem;
}
@@ -0,0 +1,888 @@
import type { INodeProperties } from 'n8n-workflow';
export const itemOperations: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['item'],
},
},
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 an item',
},
{
name: 'Delete',
value: 'delete',
description: 'Delete an item',
action: 'Delete an item',
},
{
name: 'Get',
value: 'get',
description: 'Get an item',
action: 'Get an item',
},
{
name: 'Get Many',
value: 'getAll',
description: 'Get many items',
action: 'Get many items',
},
],
default: 'upsert',
},
];
export const itemFields: INodeProperties[] = [
// ----------------------------------
// all
// ----------------------------------
{
displayName: 'Table Name or ID',
name: 'tableName',
description:
'Table to operate on. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
type: 'options',
required: true,
displayOptions: {
show: {
resource: ['item'],
},
},
default: [],
typeOptions: {
loadOptionsMethod: 'getTables',
},
},
// ----------------------------------
// upsert
// ----------------------------------
{
displayName: 'Data to Send',
name: 'dataToSend',
type: 'options',
options: [
{
name: 'Auto-Map Input Data to Columns',
value: 'autoMapInputData',
description: 'Use when node input properties match destination column names',
},
{
name: 'Define Below for Each Column',
value: 'defineBelow',
description: 'Set the value for each destination column',
},
],
displayOptions: {
show: {
operation: ['upsert'],
},
},
default: 'defineBelow',
description: 'Whether to insert the input data this node receives in the new row',
},
{
displayName: 'Inputs to Ignore',
name: 'inputsToIgnore',
type: 'string',
displayOptions: {
show: {
operation: ['upsert'],
dataToSend: ['autoMapInputData'],
},
},
default: '',
description:
'List of input properties to avoid sending, separated by commas. Leave empty to send all properties.',
placeholder: 'Enter properties...',
},
{
displayName: 'Fields to Send',
name: 'fieldsUi',
placeholder: 'Add Field',
type: 'fixedCollection',
typeOptions: {
multipleValueButtonText: 'Add Field to Send',
multipleValues: true,
},
displayOptions: {
show: {
operation: ['upsert'],
dataToSend: ['defineBelow'],
},
},
default: {},
options: [
{
displayName: 'Field',
name: 'fieldValues',
values: [
{
displayName: 'Field ID',
name: 'fieldId',
type: 'string',
default: '',
},
{
displayName: 'Field Value',
name: 'fieldValue',
type: 'string',
default: '',
},
],
},
],
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: ['item'],
operation: ['upsert'],
},
},
options: [
{
displayName: 'Expression Attribute Values',
name: 'eavUi',
description:
'Substitution tokens for attribute names in an expression. Only needed when the parameter "condition expression" is set.',
placeholder: 'Add Attribute Value',
type: 'fixedCollection',
default: {},
required: true,
typeOptions: {
multipleValues: true,
minValue: 1,
},
options: [
{
name: 'eavValues',
displayName: 'Expression Attribute Vaue',
values: [
{
displayName: 'Attribute',
name: 'attribute',
type: 'string',
default: '',
},
{
displayName: 'Type',
name: 'type',
type: 'options',
options: [
{
name: 'Number',
value: 'N',
},
{
name: 'String',
value: 'S',
},
],
default: 'S',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
},
],
},
],
},
{
displayName: 'Condition Expression',
name: 'conditionExpression',
type: 'string',
default: '',
description:
'A condition that must be satisfied in order for a conditional upsert to succeed. <a href="https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_PutItem.html">View details</a>.',
},
{
displayName: 'Expression Attribute Names',
name: 'eanUi',
placeholder: 'Add Expression',
type: 'fixedCollection',
default: {},
typeOptions: {
multipleValues: true,
},
options: [
{
name: 'eanValues',
displayName: 'Expression',
values: [
{
displayName: 'Key',
name: 'key',
type: 'string',
default: '',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
},
],
},
],
description:
'One or more substitution tokens for attribute names in an expression. <a href="https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_PutItem.html">View details</a>.',
},
],
},
// ----------------------------------
// delete
// ----------------------------------
{
displayName: 'Return',
name: 'returnValues',
type: 'options',
displayOptions: {
show: {
resource: ['item'],
operation: ['delete'],
},
},
options: [
{
name: 'Attribute Values',
value: 'ALL_OLD',
description: 'The content of the old item is returned',
},
{
name: 'Nothing',
value: 'NONE',
description: 'Nothing is returned',
},
],
default: 'NONE',
description:
'Use ReturnValues if you want to get the item attributes as they appeared before they were deleted',
},
{
displayName: 'Keys',
name: 'keysUi',
type: 'fixedCollection',
placeholder: 'Add Key',
default: {},
typeOptions: {
multipleValues: true,
},
displayOptions: {
show: {
resource: ['item'],
operation: ['delete'],
},
},
options: [
{
displayName: 'Key',
name: 'keyValues',
values: [
{
displayName: 'Key',
name: 'key',
type: 'string',
default: '',
},
{
displayName: 'Type',
name: 'type',
type: 'options',
options: [
{
name: 'Binary',
value: 'B',
},
{
name: 'Number',
value: 'N',
},
{
name: 'String',
value: 'S',
},
],
default: 'S',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
},
],
},
],
description:
"Item's primary key. For example, with a simple primary key, you only need to provide a value for the partition key. For a composite primary key, you must provide values for both the partition key and the sort key.",
},
{
displayName: 'Simplify',
name: 'simple',
type: 'boolean',
displayOptions: {
show: {
resource: ['item'],
operation: ['delete'],
returnValues: ['ALL_OLD'],
},
},
default: true,
description: 'Whether to return a simplified version of the response instead of the raw data',
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: ['item'],
operation: ['delete'],
},
},
options: [
{
displayName: 'Condition Expression',
name: 'conditionExpression',
type: 'string',
default: '',
description:
'A condition that must be satisfied in order for a conditional delete to succeed',
},
{
displayName: 'Expression Attribute Names',
name: 'eanUi',
placeholder: 'Add Expression',
type: 'fixedCollection',
default: {},
typeOptions: {
multipleValues: true,
},
options: [
{
name: 'eanValues',
displayName: 'Expression',
values: [
{
displayName: 'Key',
name: 'key',
type: 'string',
default: '',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
},
],
},
],
description:
'One or more substitution tokens for attribute names in an expression. Check <a href="https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_PutItem.html">Info</a>.',
},
{
displayName: 'Expression Attribute Values',
name: 'expressionAttributeUi',
description:
'Substitution tokens for attribute names in an expression. Only needed when the parameter "condition expression" is set.',
placeholder: 'Add Attribute Value',
type: 'fixedCollection',
default: {},
required: true,
typeOptions: {
multipleValues: true,
minValue: 1,
},
options: [
{
name: 'expressionAttributeValues',
displayName: 'Expression Attribute Value',
values: [
{
displayName: 'Attribute',
name: 'attribute',
type: 'string',
default: '',
},
{
displayName: 'Type',
name: 'type',
type: 'options',
options: [
{
name: 'Number',
value: 'N',
},
{
name: 'String',
value: 'S',
},
],
default: 'S',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
},
],
},
],
},
],
},
// ----------------------------------
// get
// ----------------------------------
{
displayName: 'Select',
name: 'select',
type: 'options',
displayOptions: {
show: {
resource: ['item'],
operation: ['get'],
},
},
options: [
{
name: 'All Attributes',
value: 'ALL_ATTRIBUTES',
},
{
name: 'All Projected Attributes',
value: 'ALL_PROJECTED_ATTRIBUTES',
},
{
name: 'Specific Attributes',
value: 'SPECIFIC_ATTRIBUTES',
description: 'Select them in Attributes to Select under Additional Fields',
},
],
default: 'ALL_ATTRIBUTES',
},
{
displayName: 'Simplify',
name: 'simple',
type: 'boolean',
displayOptions: {
show: {
resource: ['item'],
operation: ['get'],
select: ['ALL_PROJECTED_ATTRIBUTES', 'ALL_ATTRIBUTES'],
},
},
default: true,
description: 'Whether to return a simplified version of the response instead of the raw data',
},
{
displayName: 'Keys',
name: 'keysUi',
type: 'fixedCollection',
placeholder: 'Add Key',
default: {},
typeOptions: {
multipleValues: true,
},
displayOptions: {
show: {
resource: ['item'],
operation: ['get'],
},
},
options: [
{
displayName: 'Key',
name: 'keyValues',
values: [
{
displayName: 'Key',
name: 'key',
type: 'string',
default: '',
},
{
displayName: 'Type',
name: 'type',
type: 'options',
options: [
{
name: 'Binary',
value: 'B',
},
{
name: 'Number',
value: 'N',
},
{
name: 'String',
value: 'S',
},
],
default: 'S',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
},
],
},
],
description:
"Item's primary key. For example, with a simple primary key, you only need to provide a value for the partition key. For a composite primary key, you must provide values for both the partition key and the sort key.",
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: ['item'],
operation: ['get'],
},
},
options: [
{
displayName: 'Attributes to Select',
name: 'projectionExpression',
type: 'string',
// eslint-disable-next-line n8n-nodes-base/node-param-placeholder-miscased-id
placeholder: 'id, name',
default: '',
},
{
displayName: 'Expression Attribute Names',
name: 'eanUi',
placeholder: 'Add Expression',
type: 'fixedCollection',
default: {},
typeOptions: {
multipleValues: true,
},
options: [
{
name: 'eanValues',
displayName: 'Expression',
values: [
{
displayName: 'Key',
name: 'key',
type: 'string',
default: '',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
},
],
},
],
description:
'One or more substitution tokens for attribute names in an expression. <a href="https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_PutItem.html">View details</a>.',
},
{
displayName: 'Read Type',
name: 'readType',
type: 'options',
options: [
{
name: 'Strongly Consistent Read',
value: 'stronglyConsistentRead',
},
{
name: 'Eventually Consistent Read',
value: 'eventuallyConsistentRead',
},
],
default: 'eventuallyConsistentRead',
description:
'Type of read to perform on the table. <a href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.ReadConsistency.html">View details</a>.',
},
],
},
// ----------------------------------
// Get All
// ----------------------------------
{
displayName: 'Scan',
name: 'scan',
type: 'boolean',
displayOptions: {
show: {
resource: ['item'],
operation: ['getAll'],
},
},
default: false,
description:
'Whether to do an scan or query. Check <a href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/bp-query-scan.html" >differences</a>.',
},
{
displayName: 'Filter Expression',
name: 'filterExpression',
type: 'string',
displayOptions: {
show: {
scan: [true],
},
},
default: '',
description:
'A filter expression determines which items within the Scan results should be returned to you. All of the other results are discarded. Empty value will return all Scan results.',
},
{
displayName: 'Key Condition Expression',
name: 'keyConditionExpression',
description:
'Condition to determine the items to be retrieved. The condition must perform an equality test on a single partition key value, in this format: <code>partitionKeyName = :partitionkeyval</code>',
// eslint-disable-next-line n8n-nodes-base/node-param-placeholder-miscased-id
placeholder: 'id = :id',
default: '',
type: 'string',
required: true,
displayOptions: {
show: {
resource: ['item'],
operation: ['getAll'],
scan: [false],
},
},
},
{
displayName: 'Expression Attribute Values',
name: 'eavUi',
description: 'Substitution tokens for attribute names in an expression',
placeholder: 'Add Attribute Value',
type: 'fixedCollection',
default: {},
required: true,
typeOptions: {
multipleValues: true,
minValue: 1,
},
displayOptions: {
show: {
resource: ['item'],
operation: ['getAll'],
},
},
options: [
{
name: 'eavValues',
displayName: 'Expression Attribute Vaue',
values: [
{
displayName: 'Attribute',
name: 'attribute',
type: 'string',
default: '',
},
{
displayName: 'Type',
name: 'type',
type: 'options',
options: [
{
name: 'Number',
value: 'N',
},
{
name: 'String',
value: 'S',
},
],
default: 'S',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
},
],
},
],
},
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
resource: ['item'],
operation: ['getAll'],
},
},
default: false,
description: 'Whether to return all results or only up to a given limit',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
displayOptions: {
show: {
operation: ['getAll'],
returnAll: [false],
},
},
typeOptions: {
minValue: 1,
maxValue: 100,
},
default: 50,
description: 'Max number of results to return',
},
{
displayName: 'Select',
name: 'select',
type: 'options',
displayOptions: {
show: {
resource: ['item'],
operation: ['getAll'],
},
},
options: [
{
name: 'All Attributes',
value: 'ALL_ATTRIBUTES',
},
{
name: 'All Projected Attributes',
value: 'ALL_PROJECTED_ATTRIBUTES',
},
{
name: 'Count',
value: 'COUNT',
},
{
name: 'Specific Attributes',
value: 'SPECIFIC_ATTRIBUTES',
description: 'Select them in Attributes to Select under Additional Fields',
},
],
default: 'ALL_ATTRIBUTES',
},
{
displayName: 'Simplify',
name: 'simple',
type: 'boolean',
displayOptions: {
show: {
resource: ['item'],
operation: ['getAll'],
select: ['ALL_PROJECTED_ATTRIBUTES', 'ALL_ATTRIBUTES', 'SPECIFIC_ATTRIBUTES'],
},
},
default: true,
description: 'Whether to return a simplified version of the response instead of the raw data',
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: ['item'],
operation: ['getAll'],
},
},
options: [
{
displayName: 'Index Name',
name: 'indexName',
description:
'Name of the index to query. It can be any secondary local or global index on the table.',
type: 'string',
default: '',
},
{
displayName: 'Attributes to Select',
name: 'projectionExpression',
type: 'string',
default: '',
description:
'Text that identifies one or more attributes to retrieve from the table. These attributes can include scalars, sets, or elements of a JSON document. The attributes in the expression must be separated by commas.',
},
{
displayName: 'Filter Expression',
name: 'filterExpression',
type: 'string',
displayOptions: {
show: {
'/scan': [false],
},
},
default: '',
description:
'Text that contains conditions that DynamoDB applies after the Query operation, but before the data is returned. Items that do not satisfy the FilterExpression criteria are not returned.',
},
{
displayName: 'Expression Attribute Names',
name: 'eanUi',
placeholder: 'Add Expression',
type: 'fixedCollection',
default: {},
typeOptions: {
multipleValues: true,
},
options: [
{
name: 'eanValues',
displayName: 'Expression',
values: [
{
displayName: 'Key',
name: 'key',
type: 'string',
default: '',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
},
],
},
],
description:
'One or more substitution tokens for attribute names in an expression. Check <a href="https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_PutItem.html">Info</a>.',
},
],
},
];
@@ -0,0 +1,9 @@
{
"type": "object",
"properties": {
"success": {
"type": "boolean"
}
},
"version": 1
}
@@ -0,0 +1,9 @@
{
"type": "object",
"properties": {
"name": {
"type": "string"
}
},
"version": 3
}
@@ -0,0 +1,82 @@
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
import nock from 'nock';
import { credentials } from '../../../__tests__/credentials';
describe('AWS DynamoDB Node', () => {
const dynamoDbNock = nock('https://dynamodb.eu-central-1.amazonaws.com');
beforeAll(() => {
dynamoDbNock
.post('/', {
TableName: 'n8n-testing',
KeyConditionExpression: 'id = :idVal',
ExpressionAttributeValues: {
':idVal': { S: 'foo' },
},
ProjectionExpression: '#time',
ExpressionAttributeNames: {
'#time': 'timestamp',
},
Limit: 50,
Select: 'SPECIFIC_ATTRIBUTES',
})
.reply(200, {
Items: [
{
timestamp: { S: '2025-01-01' },
},
],
});
dynamoDbNock
.post(
'/',
(body) =>
body?.TableName === 'n8n-testing' &&
body?.Key?.id?.S === 'foo' &&
body?.Key?.timestamp?.S === '2025-01-01' &&
body?.ExpressionAttributeNames?.['#time'] === 'timestamp' &&
body?.ProjectionExpression === '#time',
)
.reply(200, {
Item: {
timestamp: { S: '2025-01-01' },
},
});
dynamoDbNock
.post(
'/',
(body) =>
body?.TableName === 'n8n-testing' &&
body?.Item?.id?.S === 'foo' &&
body?.Item?.timestamp?.S === '2025-01-01' &&
body?.Item?.data?.S === 'payload' &&
body?.ConditionExpression === '#d = :v' &&
body?.ExpressionAttributeNames?.['#d'] === 'data' &&
body?.ExpressionAttributeValues?.[':v']?.S === 'lorem ipsum',
)
.reply(200, {});
dynamoDbNock
.post(
'/',
(body) =>
body?.TableName === 'n8n-testing' &&
body?.Key?.id?.S === 'foo' &&
body?.Key?.timestamp?.S === '2025-01-01' &&
body?.ConditionExpression === '#d = :v' &&
body?.ExpressionAttributeNames?.['#d'] === 'data' &&
body?.ExpressionAttributeValues?.[':v']?.S === 'payload',
)
.reply(200, {});
});
afterAll(() => dynamoDbNock.done());
new NodeTestHarness().setupTests({
credentials,
workflowFiles: ['workflow.json'],
});
});
@@ -0,0 +1,261 @@
{
"name": "AWS DynamoDB",
"nodes": [
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [0, -40],
"id": "efd3f04e-8695-4f2c-8f8d-0b0166390d3f",
"name": "When clicking Execute workflow"
},
{
"parameters": {
"operation": "getAll",
"tableName": "n8n-testing",
"keyConditionExpression": "id = :idVal",
"eavUi": {
"eavValues": [
{
"attribute": ":idVal",
"value": "foo"
}
]
},
"select": "SPECIFIC_ATTRIBUTES",
"options": {
"projectionExpression": "#time",
"eanUi": {
"eanValues": [
{
"key": "#time",
"value": "timestamp"
}
]
}
}
},
"type": "n8n-nodes-base.awsDynamoDb",
"typeVersion": 1,
"position": [220, -340],
"id": "fe8a7fbf-83af-45bb-ae58-77f143ce2f30",
"name": "Get many items",
"credentials": {
"aws": {
"id": "UamouRLgIHQY0AXg",
"name": "AWS account"
}
}
},
{
"parameters": {
"operation": "get",
"tableName": "n8n-testing",
"keysUi": {
"keyValues": [
{
"key": "id",
"value": "foo"
},
{
"key": "timestamp",
"value": "2025-01-01"
}
]
},
"additionalFields": {
"projectionExpression": "#time",
"eanUi": {
"eanValues": [
{
"key": "#time",
"value": "timestamp"
}
]
}
}
},
"type": "n8n-nodes-base.awsDynamoDb",
"typeVersion": 1,
"position": [220, -140],
"id": "4cbddd91-c851-4ef6-b76a-d84d84f1d598",
"name": "Get an item",
"credentials": {
"aws": {
"id": "UamouRLgIHQY0AXg",
"name": "AWS account"
}
}
},
{
"parameters": {
"tableName": "n8n-testing",
"fieldsUi": {
"fieldValues": [
{
"fieldId": "id",
"fieldValue": "foo"
},
{
"fieldId": "timestamp",
"fieldValue": "2025-01-01"
},
{
"fieldId": "data",
"fieldValue": "payload"
}
]
},
"additionalFields": {
"eavUi": {
"eavValues": [
{
"attribute": ":v",
"value": "lorem ipsum"
}
]
},
"conditionExpression": "#d = :v",
"eanUi": {
"eanValues": [
{
"key": "#d",
"value": "data"
}
]
}
}
},
"type": "n8n-nodes-base.awsDynamoDb",
"typeVersion": 1,
"position": [220, 60],
"id": "17eee2d1-6547-4d99-b168-74d407cc252c",
"name": "Create or update an item",
"credentials": {
"aws": {
"id": "UamouRLgIHQY0AXg",
"name": "AWS account"
}
}
},
{
"parameters": {
"operation": "delete",
"tableName": "n8n-testing",
"keysUi": {
"keyValues": [
{
"key": "id",
"value": "foo"
},
{
"key": "timestamp",
"value": "2025-01-01"
}
]
},
"additionalFields": {
"conditionExpression": "#d = :v",
"eanUi": {
"eanValues": [
{
"key": "#d",
"value": "data"
}
]
},
"expressionAttributeUi": {
"expressionAttributeValues": [
{
"attribute": ":v",
"value": "payload"
}
]
}
}
},
"type": "n8n-nodes-base.awsDynamoDb",
"typeVersion": 1,
"position": [220, 260],
"id": "6d266500-16b0-4ee9-a3b2-a4300bc23b1f",
"name": "Delete an item",
"credentials": {
"aws": {
"id": "UamouRLgIHQY0AXg",
"name": "AWS account"
}
}
}
],
"pinData": {
"Get many items": [
{
"json": {
"timestamp": "2025-01-01"
}
}
],
"Get an item": [
{
"json": {
"timestamp": "2025-01-01"
}
}
],
"Create or update an item": [
{
"json": {
"id": "foo",
"timestamp": "2025-01-01",
"data": "payload"
}
}
],
"Delete an item": [
{
"json": {
"success": true
}
}
]
},
"connections": {
"When clicking Execute workflow": {
"main": [
[
{
"node": "Get many items",
"type": "main",
"index": 0
},
{
"node": "Get an item",
"type": "main",
"index": 0
},
{
"node": "Create or update an item",
"type": "main",
"index": 0
},
{
"node": "Delete an item",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1"
},
"versionId": "ada45c19-f09e-43e5-8e5c-d66f54f78d4f",
"meta": {
"templateCredsSetupCompleted": true,
"instanceId": "e115be144a6a5547dbfca93e774dfffa178aa94a181854c13e2ce5e14d195b2e"
},
"id": "Ffm7CA1AIhKGiOlK",
"tags": []
}
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid" viewBox="-40 -35 340 340"><path fill="#5294CF" d="M165.258 288.501h3.508l57.261-28.634.953-1.347V29.964l-.953-1.354L168.766 0h-3.551z"/><path fill="#1F5B98" d="M90.741 288.501h-3.557l-57.212-28.634-1.161-1.997-.589-226.742 1.75-2.518L87.184 0h3.601z"/><path fill="#2D72B8" d="M87.285 0h81.426v288.501H87.285z"/><path fill="#1A476F" d="m256 137.769-1.935-.429-27.628-2.576-.41.204-57.312-2.292h-81.43l-57.313 2.292V91.264l-.06.032.06-.128 57.313-13.28h81.43l57.312 13.28 21.069 11.199v-7.2l8.904-.974-.922-1.798-28.192-20.159-.859.279-57.312-17.759h-81.43L29.972 72.515V28.61L0 63.723v30.666l.232-.168 8.672.946v7.348L0 107.28v30.513l.232-.024 8.672.128v12.807l-7.482.112L0 150.68v30.525l8.904 4.788v7.433l-8.531.942-.373-.28v30.661l29.972 35.118v-43.901l57.313 17.759h81.43l57.481-17.811.764.335 27.821-19.862 1.219-1.979-8.904-.982v-7.284l-1.167-.466-19.043 10.265-.69 1.44-57.481 13.203v.016h-81.43v-.016l-57.313-13.259v-43.864l57.313 2.284v.056h81.43l57.312-2.34 1.305.6 26.779-2.306 1.889-.923-8.904-.128v-12.807z"/><path fill="#2D72B8" d="M226.027 215.966v43.901L256 224.749v-30.461l-29.8 21.626zm0-18.545.173-.04 29.8-16.028v-30.649l-29.973 2.757zm.173-106.213-.173-.04v43.8L256 137.769v-30.634zm0-18.521L256 94.193V63.731L226.027 28.61v43.905l.173.06z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

@@ -0,0 +1,90 @@
export interface IRequestBody {
[key: string]: string | IAttributeValue | undefined | boolean | object | number;
TableName: string;
Key?: object;
IndexName?: string;
ProjectionExpression?: string;
KeyConditionExpression?: string;
ExpressionAttributeValues?: IAttributeValue;
ConsistentRead?: boolean;
FilterExpression?: string;
Limit?: number;
ExclusiveStartKey?: IAttributeValue;
}
export interface IAttributeValue {
[attribute: string]: IAttributeValueValue;
}
export interface IAttributeValueValue {
[type: string]: string | string[] | IAttributeValue[];
}
export interface IAttributeValueUi {
attribute: string;
type: AttributeValueType;
value: string;
}
export interface IAttributeNameUi {
key: string;
value: string;
}
export type AttributeValueType =
| 'B' // binary
| 'BOOL' // boolean
| 'BS' // binary set
| 'L' // list
| 'M' // map
| 'N' // number
| 'NULL'
| 'NS' // number set
| 'S' // string
| 'SS'; // string set
export type PartitionKey = {
details: {
name: string;
type: string;
value: string;
};
};
export const EAttributeValueTypes = {
S: 'S',
SS: 'SS',
M: 'M',
L: 'L',
NS: 'NS',
N: 'N',
BOOL: 'BOOL',
B: 'B',
BS: 'BS',
NULL: 'NULL',
} as const;
export type EAttributeValueType = (typeof EAttributeValueTypes)[keyof typeof EAttributeValueTypes];
export interface IExpressionAttributeValue {
attribute: string;
type: EAttributeValueType;
value: string;
}
export type FieldsUiValues = Array<{
fieldId: string;
fieldValue: string;
}>;
export type PutItemUi = {
attribute: string;
type: 'S' | 'N';
value: string;
};
export type AdjustedPutItem = {
[attribute: string]: {
[type: string]: string;
};
};
@@ -0,0 +1,136 @@
import type { IDataObject, INodeExecutionData } from 'n8n-workflow';
import { deepCopy, assert, ApplicationError } from 'n8n-workflow';
import type {
AdjustedPutItem,
AttributeValueType,
EAttributeValueType,
IAttributeNameUi,
IAttributeValue,
IAttributeValueUi,
IAttributeValueValue,
PutItemUi,
} from './types';
const addColon = (attribute: string) =>
(attribute = attribute.charAt(0) === ':' ? attribute : `:${attribute}`);
const addPound = (key: string) => (key = key.charAt(0) === '#' ? key : `#${key}`);
export function adjustExpressionAttributeValues(eavUi: IAttributeValueUi[]) {
const eav: IAttributeValue = {};
eavUi.forEach(({ attribute, type, value }) => {
eav[addColon(attribute)] = { [type]: value } as IAttributeValueValue;
});
return eav;
}
export function adjustExpressionAttributeName(eanUi: IAttributeNameUi[]) {
const ean: { [key: string]: string } = {};
eanUi.forEach(({ key, value }) => {
ean[addPound(key)] = value;
});
return ean;
}
export function adjustPutItem(putItemUi: PutItemUi) {
const adjustedPutItem: AdjustedPutItem = {};
Object.entries(putItemUi).forEach(([attribute, value]) => {
let type: string;
if (typeof value === 'boolean') {
type = 'BOOL';
} else if (typeof value === 'object' && !Array.isArray(value) && value !== null) {
type = 'M';
} else if (isNaN(Number(value))) {
type = 'S';
} else {
type = 'N';
}
adjustedPutItem[attribute] = { [type]: value.toString() };
});
return adjustedPutItem;
}
export function simplify(item: IAttributeValue): IDataObject {
const output: IDataObject = {};
for (const [attribute, value] of Object.entries(item)) {
const [type, content] = Object.entries(value)[0] as [AttributeValueType, string];
//nedded as simplify is used in decodeItem
output[attribute] = decodeAttribute(type, content);
}
return output;
}
function decodeAttribute(type: AttributeValueType, attribute: string | IAttributeValue) {
switch (type) {
case 'BOOL':
return Boolean(attribute);
case 'N':
return Number(attribute);
case 'S':
return String(attribute);
case 'SS':
case 'NS':
return attribute;
case 'M':
assert(
typeof attribute === 'object' && !Array.isArray(attribute) && attribute !== null,
'Attribute must be an object',
);
return simplify(attribute);
default:
return null;
}
}
export function validateJSON(input: any): object {
try {
return JSON.parse(input as string);
} catch (error) {
throw new ApplicationError('Items must be a valid JSON', { level: 'warning' });
}
}
export function copyInputItem(item: INodeExecutionData, properties: string[]): IDataObject {
// Prepare the data to insert and copy it to be returned
const newItem: IDataObject = {};
for (const property of properties) {
if (item.json[property] === undefined) {
newItem[property] = null;
} else {
newItem[property] = deepCopy(item.json[property]);
}
}
return newItem;
}
export function mapToAttributeValues(item: IDataObject): void {
for (const key of Object.keys(item)) {
if (!key.startsWith(':')) {
item[`:${key}`] = item[key];
delete item[key];
}
}
}
export function decodeItem(item: IAttributeValue): IDataObject {
const _item: IDataObject = {};
for (const entry of Object.entries(item)) {
const [attribute, value]: [string, object] = entry;
const [type, content]: [string, object] = Object.entries(value)[0];
_item[attribute] = decodeAttribute(type as EAttributeValueType, content as unknown as string);
}
return _item;
}
@@ -0,0 +1,18 @@
{
"node": "n8n-nodes-base.awsElb",
"nodeVersion": "1.0",
"codexVersion": "1.0",
"categories": ["Development"],
"resources": {
"credentialDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/credentials/aws/"
}
],
"primaryDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.awselb/"
}
]
}
}
@@ -0,0 +1,460 @@
import type {
IExecuteFunctions,
IDataObject,
ILoadOptionsFunctions,
INodeExecutionData,
INodePropertyOptions,
INodeType,
INodeTypeDescription,
JsonObject,
} from 'n8n-workflow';
import { NodeConnectionTypes } from 'n8n-workflow';
import { awsApiRequestSOAP, awsApiRequestSOAPAllItems } from './GenericFunctions';
import {
listenerCertificateFields,
listenerCertificateOperations,
} from './ListenerCertificateDescription';
import { loadBalancerFields, loadBalancerOperations } from './LoadBalancerDescription';
import { awsNodeAuthOptions, awsNodeCredentials } from '../utils';
export class AwsElb implements INodeType {
description: INodeTypeDescription = {
displayName: 'AWS ELB',
name: 'awsElb',
icon: 'file:elb.svg',
group: ['output'],
version: 1,
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Sends data to AWS ELB API',
defaults: {
name: 'AWS ELB',
},
inputs: [NodeConnectionTypes.Main],
outputs: [NodeConnectionTypes.Main],
credentials: awsNodeCredentials,
properties: [
awsNodeAuthOptions,
{
displayName: 'Resource',
name: 'resource',
type: 'options',
noDataExpression: true,
options: [
{
name: 'Listener Certificate',
value: 'listenerCertificate',
},
{
name: 'Load Balancer',
value: 'loadBalancer',
},
],
default: 'loadBalancer',
},
...loadBalancerOperations,
...loadBalancerFields,
...listenerCertificateOperations,
...listenerCertificateFields,
],
};
methods = {
loadOptions: {
async getLoadBalancers(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const params = ['Version=2015-12-01'];
const data = await awsApiRequestSOAP.call(
this,
'elasticloadbalancing',
'GET',
'/?Action=DescribeLoadBalancers&' + params.join('&'),
);
let loadBalancers =
data.DescribeLoadBalancersResponse.DescribeLoadBalancersResult.LoadBalancers.member;
if (!Array.isArray(loadBalancers)) {
loadBalancers = [loadBalancers];
}
for (const loadBalancer of loadBalancers) {
const loadBalancerArn = loadBalancer.LoadBalancerArn as string;
const loadBalancerName = loadBalancer.LoadBalancerName as string;
returnData.push({
name: loadBalancerName,
value: loadBalancerArn,
});
}
return returnData;
},
async getLoadBalancerListeners(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const loadBalancerId = this.getCurrentNodeParameter('loadBalancerId') as string;
const params = ['Version=2015-12-01', 'LoadBalancerArn=' + loadBalancerId];
const data = await awsApiRequestSOAP.call(
this,
'elasticloadbalancing',
'GET',
'/?Action=DescribeListeners&' + params.join('&'),
);
let listeners = data.DescribeListenersResponse.DescribeListenersResult.Listeners.member;
if (!Array.isArray(listeners)) {
listeners = [listeners];
}
for (const listener of listeners) {
const listenerArn = listener.ListenerArn as string;
const listenerName = listener.ListenerArn as string;
returnData.push({
name: listenerArn,
value: listenerName,
});
}
return returnData;
},
async getSecurityGroups(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const body = ['Version=2016-11-15', 'Action=DescribeSecurityGroups'].join('&');
const data = await awsApiRequestSOAP.call(
this,
'ec2',
'POST',
'/',
body,
{},
{
'Content-Type': 'application/x-www-form-urlencoded',
charset: 'utf-8',
'User-Agent': 'aws-cli/1.18.124',
},
);
let securityGroups = data.DescribeSecurityGroupsResponse.securityGroupInfo.item;
if (!Array.isArray(securityGroups)) {
securityGroups = [securityGroups];
}
for (const securityGroup of securityGroups) {
const securityGroupId = securityGroup.groupId as string;
const securityGroupName = securityGroup.groupName as string;
returnData.push({
name: securityGroupName,
value: securityGroupId,
});
}
return returnData;
},
async getSubnets(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const body = ['Version=2016-11-15', 'Action=DescribeSubnets'].join('&');
const data = await awsApiRequestSOAP.call(
this,
'ec2',
'POST',
'/',
body,
{},
{
'Content-Type': 'application/x-www-form-urlencoded',
charset: 'utf-8',
'User-Agent': 'aws-cli/1.18.124',
},
);
let subnets = data.DescribeSubnetsResponse.subnetSet.item;
if (!Array.isArray(subnets)) {
subnets = [subnets];
}
for (const subnet of subnets) {
const subnetId = subnet.subnetId as string;
const subnetName = subnet.subnetId as string;
returnData.push({
name: subnetName,
value: subnetId,
});
}
return returnData;
},
},
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: IDataObject[] = [];
let responseData;
const resource = this.getNodeParameter('resource', 0);
const operation = this.getNodeParameter('operation', 0);
for (let i = 0; i < items.length; i++) {
try {
if (resource === 'listenerCertificate') {
//https://docs.aws.amazon.com/elasticloadbalancing/latest/APIReference/API_AddListenerCertificates.html
if (operation === 'add') {
const params = ['Version=2015-12-01'];
params.push(
'Certificates.member.1.CertificateArn=' +
(this.getNodeParameter('certificateId', i) as string),
);
params.push('ListenerArn=' + (this.getNodeParameter('listenerId', i) as string));
responseData = await awsApiRequestSOAP.call(
this,
'elasticloadbalancing',
'GET',
'/?Action=AddListenerCertificates&' + params.join('&'),
);
responseData =
responseData.AddListenerCertificatesResponse.AddListenerCertificatesResult
.Certificates.member;
}
//https://docs.aws.amazon.com/elasticloadbalancing/latest/APIReference/API_DescribeListenerCertificates.html
if (operation === 'getMany') {
const params = ['Version=2015-12-01'];
const returnAll = this.getNodeParameter('returnAll', 0);
const listenerId = this.getNodeParameter('listenerId', i) as string;
params.push(`ListenerArn=${listenerId}`);
if (returnAll) {
responseData = await awsApiRequestSOAPAllItems.call(
this,
'DescribeListenerCertificatesResponse.DescribeListenerCertificatesResult.Certificates.member',
'elasticloadbalancing',
'GET',
'/?Action=DescribeListenerCertificates&' + params.join('&'),
);
} else {
params.push('PageSize=' + (this.getNodeParameter('limit', 0) as unknown as string));
responseData = await awsApiRequestSOAP.call(
this,
'elasticloadbalancing',
'GET',
'/?Action=DescribeListenerCertificates&' + params.join('&'),
);
responseData =
responseData.DescribeListenerCertificatesResponse.DescribeListenerCertificatesResult
.Certificates.member;
}
}
//https://docs.aws.amazon.com/elasticloadbalancing/latest/APIReference/API_RemoveListenerCertificates.html
if (operation === 'remove') {
const params = ['Version=2015-12-01'];
params.push(
'Certificates.member.1.CertificateArn=' +
(this.getNodeParameter('certificateId', i) as string),
);
params.push('ListenerArn=' + (this.getNodeParameter('listenerId', i) as string));
responseData = await awsApiRequestSOAP.call(
this,
'elasticloadbalancing',
'GET',
'/?Action=RemoveListenerCertificates&' + params.join('&'),
);
responseData = { sucess: true };
}
}
if (resource === 'loadBalancer') {
//https://docs.aws.amazon.com/elasticloadbalancing/latest/APIReference/API_CreateLoadBalancer.html
if (operation === 'create') {
const ipAddressType = this.getNodeParameter('ipAddressType', i) as string;
const name = this.getNodeParameter('name', i) as string;
const schema = this.getNodeParameter('schema', i) as string;
const type = this.getNodeParameter('type', i) as string;
const subnets = this.getNodeParameter('subnets', i) as string[];
const additionalFields = this.getNodeParameter('additionalFields', i);
const params = ['Version=2015-12-01'];
params.push(`IpAddressType=${ipAddressType}`);
params.push(`Name=${name}`);
params.push(`Scheme=${schema}`);
params.push(`Type=${type}`);
for (let index = 1; index <= subnets.length; index++) {
params.push(`Subnets.member.${index}=${subnets[index - 1]}`);
}
if (additionalFields.securityGroups) {
const securityGroups = additionalFields.securityGroups as string[];
for (let index = 1; index <= securityGroups.length; index++) {
params.push(`SecurityGroups.member.${index}=${securityGroups[index - 1]}`);
}
}
if (additionalFields.tagsUi) {
const tags = (additionalFields.tagsUi as IDataObject).tagValues as IDataObject[];
if (tags) {
for (let index = 1; index <= tags.length; index++) {
params.push(`Tags.member.${index}.Key=${tags[index - 1].key}`);
params.push(`Tags.member.${index}.Value=${tags[index - 1].value}`);
}
}
}
responseData = await awsApiRequestSOAP.call(
this,
'elasticloadbalancing',
'GET',
'/?Action=CreateLoadBalancer&' + params.join('&'),
);
responseData =
responseData.CreateLoadBalancerResponse.CreateLoadBalancerResult.LoadBalancers.member;
}
//https://docs.aws.amazon.com/elasticloadbalancing/latest/APIReference/API_DeleteLoadBalancer.html
if (operation === 'delete') {
const params = ['Version=2015-12-01'];
params.push(
'LoadBalancerArn=' + (this.getNodeParameter('loadBalancerId', i) as string),
);
responseData = await awsApiRequestSOAP.call(
this,
'elasticloadbalancing',
'GET',
'/?Action=DeleteLoadBalancer&' + params.join('&'),
);
responseData = { success: true };
}
//https://docs.aws.amazon.com/elasticloadbalancing/latest/APIReference/API_DescribeLoadBalancers.html
if (operation === 'getMany') {
const params = ['Version=2015-12-01'];
const returnAll = this.getNodeParameter('returnAll', 0);
if (returnAll) {
const filters = this.getNodeParameter('filters', i);
if (filters.names) {
const names = (filters.names as string).split(',');
for (let index = 1; index <= names.length; index++) {
params.push(`Names.member.${index}=${names[index - 1]}`);
}
}
responseData = await awsApiRequestSOAPAllItems.call(
this,
'DescribeLoadBalancersResponse.DescribeLoadBalancersResult.LoadBalancers.member',
'elasticloadbalancing',
'GET',
'/?Action=DescribeLoadBalancers&' + params.join('&'),
);
} else {
params.push('PageSize=' + this.getNodeParameter('limit', 0).toString());
responseData = await awsApiRequestSOAP.call(
this,
'elasticloadbalancing',
'GET',
'/?Action=DescribeLoadBalancers&' + params.join('&'),
);
responseData =
responseData.DescribeLoadBalancersResponse.DescribeLoadBalancersResult.LoadBalancers
.member;
}
}
//https://docs.aws.amazon.com/elasticloadbalancing/latest/APIReference/API_DescribeLoadBalancers.html
if (operation === 'get') {
const params = ['Version=2015-12-01'];
params.push(
'LoadBalancerArns.member.1=' + (this.getNodeParameter('loadBalancerId', i) as string),
);
responseData = await awsApiRequestSOAP.call(
this,
'elasticloadbalancing',
'GET',
'/?Action=DescribeLoadBalancers&' + params.join('&'),
);
responseData =
responseData.DescribeLoadBalancersResponse.DescribeLoadBalancersResult.LoadBalancers
.member;
}
}
returnData.push(
...this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(responseData as IDataObject),
{
itemData: { item: i },
},
),
);
} catch (error) {
if (this.continueOnFail()) {
returnData.push({ error: (error as JsonObject).toString() });
continue;
}
throw error;
}
}
return [returnData as INodeExecutionData[]];
}
}
@@ -0,0 +1,160 @@
import get from 'lodash/get';
import type {
IDataObject,
IExecuteFunctions,
IHookFunctions,
ILoadOptionsFunctions,
IWebhookFunctions,
IHttpRequestOptions,
JsonObject,
IHttpRequestMethods,
} from 'n8n-workflow';
import { NodeApiError } from 'n8n-workflow';
import { parseString } from 'xml2js';
import { getAwsCredentials } from '../GenericFunctions';
export async function awsApiRequest(
this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions | IWebhookFunctions,
service: string,
method: IHttpRequestMethods,
path: string,
body?: string | Buffer,
query: IDataObject = {},
headers?: object,
_option: IDataObject = {},
_region?: string,
) {
const { credentials, credentialsType } = await getAwsCredentials(this);
const requestOptions = {
qs: {
...query,
service,
path,
},
headers,
method,
url: '',
body,
region: credentials?.region as string,
} as IHttpRequestOptions;
try {
return await this.helpers.requestWithAuthentication.call(this, credentialsType, requestOptions);
} catch (error) {
throw new NodeApiError(this.getNode(), error as JsonObject);
}
}
export async function awsApiRequestREST(
this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions,
service: string,
method: IHttpRequestMethods,
path: string,
body?: string,
query: IDataObject = {},
headers?: object,
options: IDataObject = {},
region?: string,
) {
const response = await awsApiRequest.call(
this,
service,
method,
path,
body,
query,
headers,
options,
region,
);
try {
return JSON.parse(response as string);
} catch (e) {
return response;
}
}
export async function awsApiRequestSOAP(
this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions | IWebhookFunctions,
service: string,
method: IHttpRequestMethods,
path: string,
body?: string | Buffer,
query: IDataObject = {},
headers?: object,
option: IDataObject = {},
region?: string,
) {
const response = await awsApiRequest.call(
this,
service,
method,
path,
body,
query,
headers,
option,
region,
);
try {
return await new Promise((resolve, reject) => {
parseString(response as string, { explicitArray: false }, (err, data) => {
if (err) {
return reject(err);
}
resolve(data);
});
});
} catch (e) {
return e;
}
}
export async function awsApiRequestSOAPAllItems(
this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions | IWebhookFunctions,
propertyName: string,
service: string,
method: IHttpRequestMethods,
path: string,
body?: string,
query: IDataObject = {},
headers: IDataObject = {},
option: IDataObject = {},
region?: string,
) {
const returnData: IDataObject[] = [];
let responseData;
const propertyNameArray = propertyName.split('.');
do {
responseData = await awsApiRequestSOAP.call(
this,
service,
method,
path,
body,
query,
headers,
option,
region,
);
if (get(responseData, [propertyNameArray[0], propertyNameArray[1], 'NextMarker'])) {
query.Marker = get(responseData, [propertyNameArray[0], propertyNameArray[1], 'NextMarker']);
}
if (get(responseData, propertyName)) {
if (Array.isArray(get(responseData, propertyName))) {
returnData.push.apply(returnData, get(responseData, propertyName) as IDataObject[]);
} else {
returnData.push(get(responseData, propertyName) as IDataObject);
}
}
} while (
get(responseData, [propertyNameArray[0], propertyNameArray[1], 'NextMarker']) !== undefined
);
return returnData;
}
@@ -0,0 +1,223 @@
import type { INodeProperties } from 'n8n-workflow';
export const listenerCertificateOperations: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['listenerCertificate'],
},
},
options: [
{
name: 'Add',
value: 'add',
description:
'Add the specified SSL server certificate to the certificate list for the specified HTTPS or TLS listener',
action: 'Add a listener certificate',
},
{
name: 'Get Many',
value: 'getMany',
description: 'Get many listener certificates',
action: 'Get many listener certificates',
},
{
name: 'Remove',
value: 'remove',
description:
'Remove the specified certificate from the certificate list for the specified HTTPS or TLS listener',
action: 'Remove a listener certificate',
},
],
default: 'add',
},
];
export const listenerCertificateFields: INodeProperties[] = [
/* -------------------------------------------------------------------------- */
/* listenerCertificate:add */
/* -------------------------------------------------------------------------- */
{
displayName: 'Load Balancer ARN Name or ID',
name: 'loadBalancerId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getLoadBalancers',
},
required: true,
displayOptions: {
show: {
resource: ['listenerCertificate'],
operation: ['add'],
},
},
default: '',
description:
'Unique identifier for a particular loadBalancer. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
},
{
displayName: 'Listener ARN Name or ID',
name: 'listenerId',
type: 'options',
required: true,
typeOptions: {
loadOptionsMethod: 'getLoadBalancerListeners',
loadOptionsDependsOn: ['loadBalancerId'],
},
displayOptions: {
show: {
resource: ['listenerCertificate'],
operation: ['add'],
},
},
default: '',
description:
'Unique identifier for a particular loadBalancer. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
},
{
displayName: 'Certificate ARN',
name: 'certificateId',
type: 'string',
required: true,
displayOptions: {
show: {
resource: ['listenerCertificate'],
operation: ['add'],
},
},
default: '',
description: 'Unique identifier for a particular loadBalancer',
},
/* -------------------------------------------------------------------------- */
/* listenerCertificate:getMany */
/* -------------------------------------------------------------------------- */
{
displayName: 'Load Balancer ARN Name or ID',
name: 'loadBalancerId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getLoadBalancers',
},
required: true,
displayOptions: {
show: {
resource: ['listenerCertificate'],
operation: ['getMany'],
},
},
default: '',
description:
'Unique identifier for a particular loadBalancer. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
},
{
displayName: 'Listener ARN Name or ID',
name: 'listenerId',
type: 'options',
required: true,
typeOptions: {
loadOptionsMethod: 'getLoadBalancerListeners',
loadOptionsDependsOn: ['loadBalancerId'],
},
displayOptions: {
show: {
resource: ['listenerCertificate'],
operation: ['getMany'],
},
},
default: '',
description:
'Unique identifier for a particular loadBalancer. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
},
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
resource: ['listenerCertificate'],
operation: ['getMany'],
},
},
default: false,
description: 'Whether to return all results or only up to a given limit',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
description: 'Max number of results to return',
default: 100,
typeOptions: {
maxValue: 400,
minValue: 1,
},
displayOptions: {
show: {
resource: ['listenerCertificate'],
operation: ['getMany'],
returnAll: [false],
},
},
},
/* -------------------------------------------------------------------------- */
/* listenerCertificate:remove */
/* -------------------------------------------------------------------------- */
{
displayName: 'Load Balancer ARN Name or ID',
name: 'loadBalancerId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getLoadBalancers',
},
required: true,
displayOptions: {
show: {
resource: ['listenerCertificate'],
operation: ['remove'],
},
},
default: '',
description:
'Unique identifier for a particular loadBalancer. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
},
{
displayName: 'Listener ARN Name or ID',
name: 'listenerId',
type: 'options',
required: true,
typeOptions: {
loadOptionsMethod: 'getLoadBalancerListeners',
loadOptionsDependsOn: ['loadBalancerId'],
},
displayOptions: {
show: {
resource: ['listenerCertificate'],
operation: ['remove'],
},
},
default: '',
description:
'Unique identifier for a particular loadBalancer. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
},
{
displayName: 'Certificate ARN',
name: 'certificateId',
type: 'string',
required: true,
displayOptions: {
show: {
resource: ['listenerCertificate'],
operation: ['remove'],
},
},
default: '',
description: 'Unique identifier for a particular loadBalancer',
},
];
@@ -0,0 +1,304 @@
import type { INodeProperties } from 'n8n-workflow';
export const loadBalancerOperations: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['loadBalancer'],
},
},
options: [
{
name: 'Create',
value: 'create',
description: 'Create a load balancer',
action: 'Create a load balancer',
},
{
name: 'Delete',
value: 'delete',
description: 'Delete a load balancer',
action: 'Delete a load balancer',
},
{
name: 'Get',
value: 'get',
description: 'Get a load balancer',
action: 'Get a load balancer',
},
{
name: 'Get Many',
value: 'getMany',
description: 'Get many load balancers',
action: 'Get many load balancers',
},
],
default: 'create',
},
];
export const loadBalancerFields: INodeProperties[] = [
/* -------------------------------------------------------------------------- */
/* loadBalancer:create */
/* -------------------------------------------------------------------------- */
{
displayName: 'IP Address Type',
name: 'ipAddressType',
type: 'options',
required: true,
displayOptions: {
show: {
resource: ['loadBalancer'],
operation: ['create'],
},
},
options: [
{
name: 'Ipv4',
value: 'ipv4',
},
{
name: 'Dualstack',
value: 'dualstack',
},
],
default: 'ipv4',
description: 'The type of IP addresses used by the subnets for your load balancer',
},
{
displayName: 'Name',
name: 'name',
type: 'string',
required: true,
displayOptions: {
show: {
resource: ['loadBalancer'],
operation: ['create'],
},
},
default: '',
description:
'This name must be unique per region per account, can have a maximum of 32 characters',
},
{
displayName: 'Schema',
name: 'schema',
type: 'options',
required: true,
displayOptions: {
show: {
resource: ['loadBalancer'],
operation: ['create'],
},
},
options: [
{
name: 'Internal',
value: 'internal',
},
{
name: 'Internet Facing',
value: 'internet-facing',
},
],
default: 'internet-facing',
},
{
displayName: 'Type',
name: 'type',
type: 'options',
required: true,
displayOptions: {
show: {
resource: ['loadBalancer'],
operation: ['create'],
},
},
options: [
{
name: 'Application',
value: 'application',
},
{
name: 'Network',
value: 'network',
},
],
default: 'application',
},
{
displayName: 'Subnet ID Names or IDs',
name: 'subnets',
type: 'multiOptions',
description:
'Choose from the list, or specify IDs using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
displayOptions: {
show: {
resource: ['loadBalancer'],
operation: ['create'],
},
},
typeOptions: {
loadOptionsMethod: 'getSubnets',
},
required: true,
default: [],
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
displayOptions: {
show: {
operation: ['create'],
resource: ['loadBalancer'],
},
},
default: {},
options: [
{
displayName: 'Security Group IDs',
name: 'securityGroups',
type: 'multiOptions',
description:
'Choose from the list, or specify IDs using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
typeOptions: {
loadOptionsMethod: 'getSecurityGroups',
},
default: [],
},
{
displayName: 'Tags',
name: 'tagsUi',
placeholder: 'Add Tag',
type: 'fixedCollection',
default: {},
typeOptions: {
multipleValues: true,
},
options: [
{
name: 'tagValues',
displayName: 'Tag',
values: [
{
displayName: 'Key',
name: 'key',
type: 'string',
default: '',
description: 'The key of the tag',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
description: 'The value of the tag',
},
],
},
],
},
],
},
/* -------------------------------------------------------------------------- */
/* loadBalancer:get */
/* -------------------------------------------------------------------------- */
{
displayName: 'Load Balancer ARN',
name: 'loadBalancerId',
type: 'string',
required: true,
displayOptions: {
show: {
resource: ['loadBalancer'],
operation: ['get'],
},
},
default: '',
description: 'Unique identifier for a particular loadBalancer',
},
/* -------------------------------------------------------------------------- */
/* loadBalancer:getMany */
/* -------------------------------------------------------------------------- */
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
resource: ['loadBalancer'],
operation: ['getMany'],
},
},
default: false,
description: 'Whether to return all results or only up to a given limit',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
description: 'Max number of results to return',
default: 100,
typeOptions: {
maxValue: 400,
minValue: 1,
},
displayOptions: {
show: {
resource: ['loadBalancer'],
operation: ['getMany'],
returnAll: [false],
},
},
},
{
displayName: 'Filters',
name: 'filters',
type: 'collection',
placeholder: 'Add Filter',
displayOptions: {
show: {
operation: ['getMany'],
resource: ['loadBalancer'],
returnAll: [true],
},
},
default: {},
options: [
{
displayName: 'Names',
name: 'names',
type: 'string',
default: '',
description:
'The names of the load balancers. Multiples can be defined separated by comma.',
},
],
},
/* -------------------------------------------------------------------------- */
/* loadBalancer:delete */
/* -------------------------------------------------------------------------- */
{
displayName: 'Load Balancer ARN',
name: 'loadBalancerId',
type: 'string',
required: true,
displayOptions: {
show: {
resource: ['loadBalancer'],
operation: ['delete'],
},
},
default: '',
description: 'ID of loadBalancer to delete',
},
];
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#fff" fill-rule="evenodd" stroke="#000" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 74.375 85"><style>.B{fill:#f58536}.C{fill:#9d5025}</style><use xlink:href="#A" x="2.188" y="2.5"/><symbol id="A" overflow="visible"><g stroke="none"><path d="M3.511 14.898 0 16.56v46.88l3.511 1.662L17.423 40z" class="C"/><path d="m11.694 63.285-8.183 1.817V14.898l8.183 1.769z" class="B"/><path d="m7.382 13.061 4.312-2.031L21.899 40 11.694 68.97l-4.312-2.031z" class="C"/><path d="M21.899 66.239 11.694 68.97V11.03l10.205 2.741z" class="B"/><path d="m16.499 8.747 5.4-2.556L55.616 40 21.899 73.8l-5.4-2.546z" class="C"/><path d="M58.357 61.031 21.899 73.8V6.191l36.458 12.614z" class="B"/><path fill="#6b3a19" d="m53.634 33.693-6.807.418-18.819-1.506L34.99 0l18.645 33.693z"/><path d="M34.99 31.934V0l-6.982 3.304v29.3z" class="C"/><path d="M53.635 33.693V8.824L34.99 0v31.934z" class="B"/><path fill="#fbbf93" d="m53.634 46.094-6.448-.389-19.179 1.536L34.99 80l18.645-33.907z"/><path d="M28.008 47.24v29.465L34.99 80V47.842z" class="C"/><path d="M34.99 47.842V80l18.645-8.824V46.093z" class="B"/><path fill="#fbbf93" d="m70 44.558-4.004-.156-10.38.865 4.394 22.896z"/><path d="M60.01 45.491v22.672L70 63.431V44.558z" class="B"/><path fill="#6b3a19" d="m70 35.248-4.004.155-10.38-.875 4.394-22.692z"/><path d="M70 35.248V16.57l-9.99-4.733v22.459z" class="B"/><path d="m55.616 67.065 4.394 1.098V45.491l-4.394-.224zm0-54.13 4.394-1.098v22.459l-4.394.233z" class="C"/></g></symbol></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -0,0 +1,295 @@
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
import nock from 'nock';
describe('AWS ELB', () => {
const credentials = {
aws: {
region: 'us-east-1',
accessKeyId: 'test-access-key',
secretAccessKey: 'test-secret-key',
},
};
describe('Load Balancer Operations', () => {
describe('Create Load Balancer', () => {
beforeAll(() => {
const mock = nock('https://elasticloadbalancing.us-east-1.amazonaws.com');
mock
.get(
'/?Action=CreateLoadBalancer&Version=2015-12-01&IpAddressType=ipv4&Name=test-lb&Scheme=internet-facing&Type=application&Subnets.member.1=subnet-12345&Subnets.member.2=subnet-67890&SecurityGroups.member.1=sg-12345&Tags.member.1.Key=Environment&Tags.member.1.Value=test',
)
.reply(
200,
`
<CreateLoadBalancerResponse>
<CreateLoadBalancerResult>
<LoadBalancers>
<member>
<LoadBalancerArn>arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/test-lb/1234567890123456</LoadBalancerArn>
<LoadBalancerName>test-lb</LoadBalancerName>
<Scheme>internet-facing</Scheme>
<Type>application</Type>
<State>
<Code>provisioning</Code>
</State>
</member>
</LoadBalancers>
</CreateLoadBalancerResult>
</CreateLoadBalancerResponse>
`,
);
});
new NodeTestHarness().setupTests({
credentials,
workflowFiles: ['create-load-balancer.workflow.json'],
});
});
describe('Get Load Balancer', () => {
beforeAll(() => {
const mock = nock('https://elasticloadbalancing.us-east-1.amazonaws.com');
mock
.get(
'/?Action=DescribeLoadBalancers&Version=2015-12-01&LoadBalancerArns.member.1=arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/test-lb/1234567890123456',
)
.reply(
200,
`
<DescribeLoadBalancersResponse>
<DescribeLoadBalancersResult>
<LoadBalancers>
<member>
<LoadBalancerArn>arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/test-lb/1234567890123456</LoadBalancerArn>
<LoadBalancerName>test-lb</LoadBalancerName>
<Scheme>internet-facing</Scheme>
<Type>application</Type>
<State>
<Code>active</Code>
</State>
</member>
</LoadBalancers>
</DescribeLoadBalancersResult>
</DescribeLoadBalancersResponse>
`,
);
});
new NodeTestHarness().setupTests({
credentials,
workflowFiles: ['get-load-balancer.workflow.json'],
});
});
describe('Get Many Load Balancers', () => {
beforeAll(() => {
const mock = nock('https://elasticloadbalancing.us-east-1.amazonaws.com');
mock.get('/?Action=DescribeLoadBalancers&Version=2015-12-01&PageSize=10').reply(
200,
`
<DescribeLoadBalancersResponse>
<DescribeLoadBalancersResult>
<LoadBalancers>
<member>
<LoadBalancerArn>arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/test-lb-1/1234567890123456</LoadBalancerArn>
<LoadBalancerName>test-lb-1</LoadBalancerName>
<Scheme>internet-facing</Scheme>
<Type>application</Type>
</member>
<member>
<LoadBalancerArn>arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/net/test-lb-2/2345678901234567</LoadBalancerArn>
<LoadBalancerName>test-lb-2</LoadBalancerName>
<Scheme>internal</Scheme>
<Type>network</Type>
</member>
</LoadBalancers>
</DescribeLoadBalancersResult>
</DescribeLoadBalancersResponse>
`,
);
mock
.get(
'/?Action=DescribeLoadBalancers&Version=2015-12-01&Names.member.1=test-lb-1&Names.member.2=test-lb-2',
)
.reply(
200,
`
<DescribeLoadBalancersResponse>
<DescribeLoadBalancersResult>
<LoadBalancers>
<member>
<LoadBalancerArn>arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/test-lb-1/1234567890123456</LoadBalancerArn>
<LoadBalancerName>test-lb-1</LoadBalancerName>
<Scheme>internet-facing</Scheme>
<Type>application</Type>
</member>
<member>
<LoadBalancerArn>arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/net/test-lb-2/2345678901234567</LoadBalancerArn>
<LoadBalancerName>test-lb-2</LoadBalancerName>
<Scheme>internal</Scheme>
<Type>network</Type>
</member>
</LoadBalancers>
</DescribeLoadBalancersResult>
</DescribeLoadBalancersResponse>
`,
);
});
new NodeTestHarness().setupTests({
credentials,
workflowFiles: [
'get-many-load-balancers.workflow.json',
'get-many-load-balancers-with-names.workflow.json',
],
});
});
describe('Delete Load Balancer', () => {
beforeAll(() => {
const mock = nock('https://elasticloadbalancing.us-east-1.amazonaws.com');
mock
.get(
'/?Action=DeleteLoadBalancer&Version=2015-12-01&LoadBalancerArn=arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/test-lb/1234567890123456',
)
.reply(
200,
`
<DeleteLoadBalancerResponse>
<DeleteLoadBalancerResult/>
</DeleteLoadBalancerResponse>
`,
);
});
new NodeTestHarness().setupTests({
credentials,
workflowFiles: ['delete-load-balancer.workflow.json'],
});
});
});
describe('Listener Certificate Operations', () => {
describe('Add Listener Certificate', () => {
beforeAll(() => {
const mock = nock('https://elasticloadbalancing.us-east-1.amazonaws.com');
mock
.get(
'/?Action=AddListenerCertificates&Version=2015-12-01&Certificates.member.1.CertificateArn=arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012&ListenerArn=arn:aws:elasticloadbalancing:us-east-1:123456789012:listener/app/test-lb/1234567890123456/f2f7dc8efc522ab2',
)
.reply(
200,
`
<AddListenerCertificatesResponse>
<AddListenerCertificatesResult>
<Certificates>
<member>
<CertificateArn>arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012</CertificateArn>
<IsDefault>false</IsDefault>
</member>
</Certificates>
</AddListenerCertificatesResult>
</AddListenerCertificatesResponse>
`,
);
});
new NodeTestHarness().setupTests({
credentials,
workflowFiles: ['add-listener-certificate.workflow.json'],
});
});
describe('Get Many Listener Certificates', () => {
beforeAll(() => {
const mock = nock('https://elasticloadbalancing.us-east-1.amazonaws.com');
mock
.get(
'/?Action=DescribeListenerCertificates&Version=2015-12-01&ListenerArn=arn:aws:elasticloadbalancing:us-east-1:123456789012:listener/app/test-lb/1234567890123456/f2f7dc8efc522ab2&PageSize=10',
)
.reply(
200,
`
<DescribeListenerCertificatesResponse>
<DescribeListenerCertificatesResult>
<Certificates>
<member>
<CertificateArn>arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012</CertificateArn>
<IsDefault>false</IsDefault>
</member>
<member>
<CertificateArn>arn:aws:acm:us-east-1:123456789012:certificate/87654321-4321-4321-4321-210987654321</CertificateArn>
<IsDefault>true</IsDefault>
</member>
</Certificates>
</DescribeListenerCertificatesResult>
</DescribeListenerCertificatesResponse>
`,
);
mock
.get(
'/?Action=DescribeListenerCertificates&Version=2015-12-01&ListenerArn=arn:aws:elasticloadbalancing:us-east-1:123456789012:listener/app/test-lb/1234567890123456/f2f7dc8efc522ab2',
)
.reply(
200,
`
<DescribeListenerCertificatesResponse>
<DescribeListenerCertificatesResult>
<Certificates>
<member>
<CertificateArn>arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012</CertificateArn>
<IsDefault>false</IsDefault>
</member>
<member>
<CertificateArn>arn:aws:acm:us-east-1:123456789012:certificate/87654321-4321-4321-4321-210987654321</CertificateArn>
<IsDefault>true</IsDefault>
</member>
</Certificates>
</DescribeListenerCertificatesResult>
</DescribeListenerCertificatesResponse>
`,
);
});
new NodeTestHarness().setupTests({
credentials,
workflowFiles: [
'get-many-listener-certificates.workflow.json',
'get-many-listener-certificates-all.workflow.json',
],
});
});
describe('Remove Listener Certificate', () => {
beforeAll(() => {
const mock = nock('https://elasticloadbalancing.us-east-1.amazonaws.com');
mock
.get(
'/?Action=RemoveListenerCertificates&Version=2015-12-01&Certificates.member.1.CertificateArn=arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012&ListenerArn=arn:aws:elasticloadbalancing:us-east-1:123456789012:listener/app/test-lb/1234567890123456/f2f7dc8efc522ab2',
)
.reply(
200,
`
<RemoveListenerCertificatesResponse>
<RemoveListenerCertificatesResult/>
</RemoveListenerCertificatesResponse>
`,
);
});
new NodeTestHarness().setupTests({
credentials,
workflowFiles: ['remove-listener-certificate.workflow.json'],
});
});
});
});
@@ -0,0 +1,289 @@
import { mockDeep } from 'jest-mock-extended';
import type { IExecuteFunctions } from 'n8n-workflow';
import { NodeApiError } from 'n8n-workflow';
import {
awsApiRequest,
awsApiRequestREST,
awsApiRequestSOAP,
awsApiRequestSOAPAllItems,
} from '../GenericFunctions';
jest.mock('xml2js', () => ({
parseString: jest.fn(),
}));
import { parseString as parseXml } from 'xml2js';
describe('ELB GenericFunctions', () => {
describe('awsApiRequest', () => {
let mockExecuteFunctions: jest.Mocked<IExecuteFunctions>;
beforeEach(() => {
mockExecuteFunctions = mockDeep<IExecuteFunctions>();
jest.clearAllMocks();
mockExecuteFunctions.getNode.mockReturnValue({
id: 'test-node',
name: 'Test ELB Node',
type: 'n8n-nodes-base.awsElb',
typeVersion: 1,
position: [0, 0],
parameters: {},
});
});
it('should make successful API request with basic parameters', async () => {
const mockResponse = { success: true };
const mockRequestWithAuth = mockExecuteFunctions.helpers
.requestWithAuthentication as jest.Mock;
mockRequestWithAuth.mockResolvedValue(mockResponse);
const result = await awsApiRequest.call(
mockExecuteFunctions,
'elasticloadbalancing',
'GET',
'/test-path',
);
expect(result).toEqual(mockResponse);
expect(mockRequestWithAuth).toHaveBeenCalledWith(
'aws',
expect.objectContaining({
qs: expect.objectContaining({
service: 'elasticloadbalancing',
path: '/test-path',
}),
method: 'GET',
}),
);
});
it('should handle API errors', async () => {
const apiError = new Error('API Error');
const mockRequestWithAuth = mockExecuteFunctions.helpers
.requestWithAuthentication as jest.Mock;
mockRequestWithAuth.mockRejectedValue(apiError);
await expect(
awsApiRequest.call(mockExecuteFunctions, 'elasticloadbalancing', 'GET', '/test-path'),
).rejects.toThrow(NodeApiError);
});
});
describe('awsApiRequestREST', () => {
let mockExecuteFunctions: jest.Mocked<IExecuteFunctions>;
beforeEach(() => {
mockExecuteFunctions = mockDeep<IExecuteFunctions>();
jest.clearAllMocks();
});
it('should parse valid JSON response', async () => {
const jsonResponse = '{"result": "success", "data": [1,2,3]}';
const expectedResult = { result: 'success', data: [1, 2, 3] };
const mockRequestWithAuth = mockExecuteFunctions.helpers
.requestWithAuthentication as jest.Mock;
mockRequestWithAuth.mockResolvedValue(jsonResponse);
const result = await awsApiRequestREST.call(
mockExecuteFunctions,
'elasticloadbalancing',
'GET',
'/test-path',
);
expect(result).toEqual(expectedResult);
});
it('should return raw response when JSON parsing fails', async () => {
const rawResponse = 'not json data';
const mockRequestWithAuth = mockExecuteFunctions.helpers
.requestWithAuthentication as jest.Mock;
mockRequestWithAuth.mockResolvedValue(rawResponse);
const result = await awsApiRequestREST.call(
mockExecuteFunctions,
'elasticloadbalancing',
'GET',
'/test-path',
);
expect(result).toBe(rawResponse);
});
});
describe('awsApiRequestSOAP', () => {
let mockExecuteFunctions: jest.Mocked<IExecuteFunctions>;
const mockParseXml = parseXml as jest.MockedFunction<typeof parseXml>;
beforeEach(() => {
mockExecuteFunctions = mockDeep<IExecuteFunctions>();
jest.clearAllMocks();
});
it('should parse XML response correctly', async () => {
const xmlResponse =
'<ListBucketsResult><Buckets><Bucket><Name>test-bucket</Name></Bucket></Buckets></ListBucketsResult>';
const expectedParsedData = {
ListBucketsResult: {
Buckets: {
Bucket: {
Name: 'test-bucket',
},
},
},
};
(mockExecuteFunctions.helpers.requestWithAuthentication as jest.Mock).mockResolvedValue(
xmlResponse,
);
mockParseXml.mockImplementation((_xml, _options, callback) => {
callback(null, expectedParsedData);
});
const result = await awsApiRequestSOAP.call(
mockExecuteFunctions,
'elasticloadbalancing',
'GET',
'/test-path',
);
expect(result).toEqual(expectedParsedData);
});
it('should handle XML parsing errors', async () => {
const xmlResponse = 'invalid xml';
const xmlError = new Error('Invalid XML');
(mockExecuteFunctions.helpers.requestWithAuthentication as jest.Mock).mockResolvedValue(
xmlResponse,
);
mockParseXml.mockImplementation((_xml, _options, callback) => {
callback(xmlError, null);
});
const result = await awsApiRequestSOAP.call(
mockExecuteFunctions,
'elasticloadbalancing',
'GET',
'/test-path',
);
expect(result).toBeInstanceOf(Error);
});
});
describe('awsApiRequestSOAPAllItems', () => {
let mockExecuteFunctions: jest.Mocked<IExecuteFunctions>;
const mockParseXml = parseXml as jest.MockedFunction<typeof parseXml>;
beforeEach(() => {
mockExecuteFunctions = mockDeep<IExecuteFunctions>();
jest.clearAllMocks();
});
describe('pagination patterns', () => {
it('should handle DescribeLoadBalancers pagination', async () => {
const mockCredentials = { region: 'us-east-1' };
const xmlResponse = {
DescribeLoadBalancersResponse: {
DescribeLoadBalancersResult: {
LoadBalancerDescriptions: [
{ LoadBalancerName: 'elb-1' },
{ LoadBalancerName: 'elb-2' },
],
},
},
};
mockExecuteFunctions.getCredentials.mockResolvedValue(mockCredentials);
(mockExecuteFunctions.helpers.requestWithAuthentication as jest.Mock).mockResolvedValue(
'<xml>response</xml>',
);
mockParseXml.mockImplementation((_xml, _options, callback) => {
callback(null, xmlResponse);
});
const result = await awsApiRequestSOAPAllItems.call(
mockExecuteFunctions,
'DescribeLoadBalancersResponse.DescribeLoadBalancersResult.LoadBalancerDescriptions',
'elasticloadbalancing',
'POST',
'/describe-load-balancers',
);
expect(result).toEqual([{ LoadBalancerName: 'elb-1' }, { LoadBalancerName: 'elb-2' }]);
});
it('should handle DescribeTargetGroups with nested properties', async () => {
const mockCredentials = { region: 'us-east-1' };
const firstResponse = {
DescribeTargetGroupsResponse: {
DescribeTargetGroupsResult: {
TargetGroups: [
{ TargetGroupName: 'tg-1', Protocol: 'HTTP' },
{ TargetGroupName: 'tg-2', Protocol: 'HTTPS' },
],
NextMarker: 'marker123',
},
},
};
const secondResponse = {
DescribeTargetGroupsResponse: {
DescribeTargetGroupsResult: {
TargetGroups: [{ TargetGroupName: 'tg-3', Protocol: 'TCP' }],
},
},
};
mockExecuteFunctions.getCredentials.mockResolvedValue(mockCredentials);
(mockExecuteFunctions.helpers.requestWithAuthentication as jest.Mock)
.mockResolvedValueOnce('<xml>first-response</xml>')
.mockResolvedValueOnce('<xml>second-response</xml>');
mockParseXml
.mockImplementationOnce((_xml, _options, callback) => callback(null, firstResponse))
.mockImplementationOnce((_xml, _options, callback) => callback(null, secondResponse));
const result = await awsApiRequestSOAPAllItems.call(
mockExecuteFunctions,
'DescribeTargetGroupsResponse.DescribeTargetGroupsResult.TargetGroups',
'elasticloadbalancing',
'POST',
'/describe-target-groups',
);
expect(result).toEqual([
{ TargetGroupName: 'tg-1', Protocol: 'HTTP' },
{ TargetGroupName: 'tg-2', Protocol: 'HTTPS' },
{ TargetGroupName: 'tg-3', Protocol: 'TCP' },
]);
});
});
describe('error handling', () => {
it('should handle service error responses', async () => {
const mockCredentials = { region: 'us-east-1' };
const elbError = new Error(
'LoadBalancerNotFound: The specified load balancer does not exist',
);
mockExecuteFunctions.getCredentials.mockResolvedValue(mockCredentials);
(mockExecuteFunctions.helpers.requestWithAuthentication as jest.Mock).mockRejectedValue(
elbError,
);
await expect(
awsApiRequestSOAPAllItems.call(
mockExecuteFunctions,
'DescribeLoadBalancersResponse.DescribeLoadBalancersResult.LoadBalancerDescriptions',
'elasticloadbalancing',
'POST',
'/describe-load-balancers',
),
).rejects.toThrow('LoadBalancerNotFound: The specified load balancer does not exist');
});
});
});
});
@@ -0,0 +1,29 @@
# AWS ELB Test Implementation Notes
This document explains intentional test data adjustments made to match the current implementation behavior.
## Known Implementation Issues
### 1. Typo in Remove Listener Certificate Operation
**File**: `remove-listener-certificate.workflow.json`
**Issue**: The response contains `"sucess": true` instead of `"success": true`
**Root Cause**: Spelling error in the node implementation
**Test Adjustment**: The pinData uses the misspelled version to match actual output
**TODO**: Fix the typo in the AWS ELB node implementation and update test accordingly
### 2. Boolean Values Returned as Strings
**File**: `get-many-listener-certificates-all.workflow.json`
**Issue**: Boolean values like `IsDefault` are returned as strings ("true"/"false") instead of actual booleans
**Root Cause**: AWS API response parsing returns string values rather than converting to boolean types
**Test Adjustment**: The pinData uses string values ("true"/"false") instead of boolean values (true/false)
**Note**: This reflects how the AWS API response is actually parsed and returned by the node
## Test Data Maintenance
When updating these test files:
1. Verify the actual node implementation output before changing expected results
2. If implementation bugs are fixed, update both the node code and corresponding test data
3. Consider backward compatibility when making changes that affect output format
@@ -0,0 +1,60 @@
{
"name": "AWS ELB Add Listener Certificate Test Workflow",
"nodes": [
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [0, 0],
"id": "trigger-id",
"name": "When clicking 'Execute Workflow'"
},
{
"parameters": {
"resource": "listenerCertificate",
"operation": "add",
"loadBalancerId": "arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/test-lb/1234567890123456",
"listenerId": "arn:aws:elasticloadbalancing:us-east-1:123456789012:listener/app/test-lb/1234567890123456/f2f7dc8efc522ab2",
"certificateId": "arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012"
},
"type": "n8n-nodes-base.awsElb",
"typeVersion": 1,
"position": [200, 0],
"id": "node-id",
"name": "Add Listener Certificate",
"credentials": {
"aws": {
"id": "credential-id",
"name": "Test AWS Credentials"
}
}
}
],
"pinData": {
"Add Listener Certificate": [
{
"json": {
"CertificateArn": "arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012",
"IsDefault": "false"
}
}
]
},
"connections": {
"When clicking 'Execute Workflow'": {
"main": [
[
{
"node": "Add Listener Certificate",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1"
}
}
@@ -0,0 +1,78 @@
{
"name": "AWS ELB Create Load Balancer Test Workflow",
"nodes": [
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [0, 0],
"id": "trigger-id",
"name": "When clicking 'Execute Workflow'"
},
{
"parameters": {
"resource": "loadBalancer",
"operation": "create",
"ipAddressType": "ipv4",
"name": "test-lb",
"schema": "internet-facing",
"type": "application",
"subnets": ["subnet-12345", "subnet-67890"],
"additionalFields": {
"securityGroups": ["sg-12345"],
"tagsUi": {
"tagValues": [
{
"key": "Environment",
"value": "test"
}
]
}
}
},
"type": "n8n-nodes-base.awsElb",
"typeVersion": 1,
"position": [200, 0],
"id": "node-id",
"name": "Create Load Balancer",
"credentials": {
"aws": {
"id": "credential-id",
"name": "Test AWS Credentials"
}
}
}
],
"pinData": {
"Create Load Balancer": [
{
"json": {
"LoadBalancerArn": "arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/test-lb/1234567890123456",
"LoadBalancerName": "test-lb",
"Scheme": "internet-facing",
"Type": "application",
"State": {
"Code": "provisioning"
}
}
}
]
},
"connections": {
"When clicking 'Execute Workflow'": {
"main": [
[
{
"node": "Create Load Balancer",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1"
}
}
@@ -0,0 +1,57 @@
{
"name": "AWS ELB Delete Load Balancer Test Workflow",
"nodes": [
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [0, 0],
"id": "trigger-id",
"name": "When clicking 'Execute Workflow'"
},
{
"parameters": {
"resource": "loadBalancer",
"operation": "delete",
"loadBalancerId": "arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/test-lb/1234567890123456"
},
"type": "n8n-nodes-base.awsElb",
"typeVersion": 1,
"position": [200, 0],
"id": "node-id",
"name": "Delete Load Balancer",
"credentials": {
"aws": {
"id": "credential-id",
"name": "Test AWS Credentials"
}
}
}
],
"pinData": {
"Delete Load Balancer": [
{
"json": {
"success": true
}
}
]
},
"connections": {
"When clicking 'Execute Workflow'": {
"main": [
[
{
"node": "Delete Load Balancer",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1"
}
}
@@ -0,0 +1,63 @@
{
"name": "AWS ELB Get Load Balancer Test Workflow",
"nodes": [
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [0, 0],
"id": "trigger-id",
"name": "When clicking 'Execute Workflow'"
},
{
"parameters": {
"resource": "loadBalancer",
"operation": "get",
"loadBalancerId": "arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/test-lb/1234567890123456"
},
"type": "n8n-nodes-base.awsElb",
"typeVersion": 1,
"position": [200, 0],
"id": "node-id",
"name": "Get Load Balancer",
"credentials": {
"aws": {
"id": "credential-id",
"name": "Test AWS Credentials"
}
}
}
],
"pinData": {
"Get Load Balancer": [
{
"json": {
"LoadBalancerArn": "arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/test-lb/1234567890123456",
"LoadBalancerName": "test-lb",
"Scheme": "internet-facing",
"Type": "application",
"State": {
"Code": "active"
}
}
}
]
},
"connections": {
"When clicking 'Execute Workflow'": {
"main": [
[
{
"node": "Get Load Balancer",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1"
}
}
@@ -0,0 +1,66 @@
{
"name": "AWS ELB Get Many Listener Certificates All Test Workflow",
"nodes": [
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [0, 0],
"id": "trigger-id",
"name": "When clicking 'Execute Workflow'"
},
{
"parameters": {
"resource": "listenerCertificate",
"operation": "getMany",
"loadBalancerId": "arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/test-lb/1234567890123456",
"listenerId": "arn:aws:elasticloadbalancing:us-east-1:123456789012:listener/app/test-lb/1234567890123456/f2f7dc8efc522ab2",
"returnAll": true
},
"type": "n8n-nodes-base.awsElb",
"typeVersion": 1,
"position": [200, 0],
"id": "node-id",
"name": "Get Many Listener Certificates All",
"credentials": {
"aws": {
"id": "credential-id",
"name": "Test AWS Credentials"
}
}
}
],
"pinData": {
"Get Many Listener Certificates All": [
{
"json": {
"CertificateArn": "arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012",
"IsDefault": "false"
}
},
{
"json": {
"CertificateArn": "arn:aws:acm:us-east-1:123456789012:certificate/87654321-4321-4321-4321-210987654321",
"IsDefault": "true"
}
}
]
},
"connections": {
"When clicking 'Execute Workflow'": {
"main": [
[
{
"node": "Get Many Listener Certificates All",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1"
}
}

Some files were not shown because too many files have changed in this diff Show More