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