Add AWS DynamoDB node (#1602)

* dynamodb: request item

* dynamodb: query database

* dynamodb: scans table

* dynamodb: typehints in decodeItem

* dynamodb: recursively decodes map and lists

* dynamodb: cleanup interface, using additional-fields collection

* dynamodb: using fixedCollection for ExpressionAttributeValues

* dynamodb: converts spaces to tabs

* dynamodb: scans with FilterExpression

* dynamodb: fixes tslint

*  Refactor node

* 🔨 Refactor into separate dir

*  Add table name loader

* ✏️ Update operation descriptions

*  Add partition key name param to delete

*  Add params to get operation

* 🔨 Refactor get operation per feedback

*  Refactor for consistency

*  Add createUpdate operation

* aja

* asasa

* aja

* aja

* aja

*  Improvements

*  Lint node

*  Lint description

* 🔥 Remove unused option

*  Apply David's feedback

* ✏️ Add descriptions for specific attributes

* 🔨 Rename return values

* Implement define and automap

*  Minior changes

Co-authored-by: Michael Hirschler <michael.vhirsch@gmail.com>
Co-authored-by: ricardo <ricardoespinoza105@gmail.com>
Co-authored-by: Jan Oberhauser <jan.oberhauser@gmail.com>
This commit is contained in:
Iván Ovejero 2021-07-17 14:14:12 +02:00 committed by GitHub
parent ca1bbcea5d
commit 9118e090e7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 1629 additions and 0 deletions

View file

@ -0,0 +1,374 @@
import {
IExecuteFunctions,
} from 'n8n-core';
import {
IDataObject,
ILoadOptionsFunctions,
INodeExecutionData,
INodeType,
INodeTypeDescription,
NodeParameterValue,
} from 'n8n-workflow';
import {
awsApiRequest,
awsApiRequestAllItems,
} from './GenericFunctions';
import {
itemFields,
itemOperations,
} from './ItemDescription';
import {
FieldsUiValues,
IAttributeNameUi,
IAttributeValueUi,
IRequestBody,
PutItemUi,
} from './types';
import {
adjustExpressionAttributeName,
adjustExpressionAttributeValues,
adjustPutItem,
decodeItem,
simplify,
} 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',
defaults: {
name: 'AWS DynamoDB',
color: '#2273b9',
},
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'aws',
required: true,
},
],
properties: [
{
displayName: 'Resource',
name: 'resource',
type: 'options',
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: IDataObject[] = [];
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('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
// tslint:disable-next-line: no-any
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.eavUi.eavValues', i, []) as IAttributeValueUi[];
const eanUi = this.getNodeParameter('additionalFields.eanUi.eanValues', i, []) as IAttributeNameUi[];
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
const simple = this.getNodeParameter('simple', 0, false) as boolean;
const items = this.getNodeParameter('keysUi.keyValues', i, []) as [{ key: string, type: string, value: string }];
for (const item of items) {
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 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).length) {
responseData = { success: true };
} else if (simple === true) {
responseData = decodeItem(responseData.Attributes);
}
} 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) as IDataObject;
const eanUi = this.getNodeParameter('additionalFields.eanUi.eanValues', i, []) as IAttributeNameUi[];
// tslint:disable-next-line: no-any
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 items = this.getNodeParameter('keysUi.keyValues', i, []) as IDataObject[];
for (const item of items) {
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);
}
} 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) as boolean;
const eanUi = this.getNodeParameter('additionalFields.eanUi.eanValues', i, []) as IAttributeNameUi[];
const body: IRequestBody = {
TableName: this.getNodeParameter('tableName', i) as string,
KeyConditionExpression: this.getNodeParameter('keyConditionExpression', i) as string,
ExpressionAttributeValues: adjustExpressionAttributeValues(eavUi),
};
const {
indexName,
projectionExpression,
} = this.getNodeParameter('options', i) as {
indexName: string;
projectionExpression: string;
};
const expressionAttributeName = adjustExpressionAttributeName(eanUi);
if (Object.keys(expressionAttributeName).length) {
body.expressionAttributeNames = expressionAttributeName;
}
if (indexName) {
body.IndexName = indexName;
}
if (projectionExpression && select !== 'COUNT') {
body.ProjectionExpression = projectionExpression;
}
if (select) {
body.Select = select;
}
const headers = {
'Content-Type': 'application/json',
'X-Amz-Target': 'DynamoDB_20120810.Query',
};
if (returnAll === true && select !== 'COUNT') {
responseData = await awsApiRequestAllItems.call(this, 'dynamodb', 'POST', '/', body, headers);
} else {
body.Limit = this.getNodeParameter('limit', 0, 1) as number;
responseData = await awsApiRequest.call(this, 'dynamodb', 'POST', '/', body, headers);
if (select !== 'COUNT') {
responseData = responseData.Items;
}
}
if (simple === true) {
responseData = responseData.map(simplify);
}
}
Array.isArray(responseData)
? returnData.push(...responseData)
: returnData.push(responseData);
}
} catch (error) {
if (this.continueOnFail()) {
returnData.push({ error: error.message });
continue;
}
throw error;
}
}
return [this.helpers.returnJsonArray(returnData)];
}
}

View file

@ -0,0 +1,108 @@
import {
URL,
} from 'url';
import {
sign,
} from 'aws4';
import {
IExecuteFunctions,
IHookFunctions,
ILoadOptionsFunctions,
IWebhookFunctions,
} from 'n8n-core';
import {
ICredentialDataDecryptedObject,
IDataObject,
INodeExecutionData,
} from 'n8n-workflow';
import {
IRequestBody,
} from './types';
function getEndpointForService(service: string, credentials: ICredentialDataDecryptedObject): string {
let endpoint;
if (service === 'lambda' && credentials.lambdaEndpoint) {
endpoint = credentials.lambdaEndpoint;
} else if (service === 'sns' && credentials.snsEndpoint) {
endpoint = credentials.snsEndpoint;
} else {
endpoint = `https://${service}.${credentials.region}.amazonaws.com`;
}
return (endpoint as string).replace('{region}', credentials.region as string);
}
export async function awsApiRequest(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions | IWebhookFunctions, service: string, method: string, path: string, body?: object | IRequestBody, headers?: object): Promise<any> { // tslint:disable-line:no-any
const credentials = this.getCredentials('aws');
if (credentials === undefined) {
throw new Error('No credentials got returned!');
}
// Concatenate path and instantiate URL object so it parses correctly query strings
const endpoint = new URL(getEndpointForService(service, credentials) + path);
const options = sign({
uri: endpoint,
service,
region: credentials.region,
method,
path: '/',
headers: { ...headers },
body: JSON.stringify(body),
}, {
accessKeyId: credentials.accessKeyId,
secretAccessKey: credentials.secretAccessKey,
});
try {
return JSON.parse(await this.helpers.request!(options));
} catch (error) {
const errorMessage = (error.response && error.response.body.message) || (error.response && error.response.body.Message) || error.message;
if (error.statusCode === 403) {
if (errorMessage === 'The security token included in the request is invalid.') {
throw new Error('The AWS credentials are not valid!');
} else if (errorMessage.startsWith('The request signature we calculated does not match the signature you provided')) {
throw new Error('The AWS credentials are not valid!');
}
}
throw new Error(`AWS error response [${error.statusCode}]: ${errorMessage}`);
}
}
export async function awsApiRequestAllItems(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions | IWebhookFunctions, service: string, method: string, path: string, body?: IRequestBody, headers?: object): Promise<any> { // tslint:disable-line:no-any
const returnData: IDataObject[] = [];
let responseData;
do {
responseData = await awsApiRequest.call(this, service, method, path, body, headers);
if (responseData.LastEvaluatedKey) {
body!.ExclusiveStartKey = responseData.LastEvaluatedKey;
}
returnData.push(...responseData.Items);
} 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
let newItem: IDataObject;
newItem = {};
for (const property of properties) {
if (item.json[property] === undefined) {
newItem[property] = null;
} else {
newItem[property] = JSON.parse(JSON.stringify(item.json[property]));
}
}
return newItem;
}

View file

@ -0,0 +1,920 @@
import {
INodeProperties,
} from 'n8n-workflow';
export const itemOperations = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
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/put)',
},
{
name: 'Delete',
value: 'delete',
description: 'Delete an item',
},
{
name: 'Get',
value: 'get',
description: 'Get an item',
},
{
name: 'Get All',
value: 'getAll',
description: 'Get all items',
},
],
default: 'upsert',
},
] as INodeProperties[];
export const itemFields = [
// ----------------------------------
// all
// ----------------------------------
{
displayName: 'Table Name',
name: 'tableName',
description: 'Table to operate on',
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: '',
required: false,
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.<br>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 target="_blank" 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 target="_blank" 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.<br>For a composite primary key, you must provide values for both the partition key and the sort key',
},
{
displayName: 'Simple',
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" target="_blank">Info</a>',
},
{
displayName: 'Expression Attribute Values',
name: 'expressionAttributeUi',
description: 'Substitution tokens for attribute names in an expression.<br>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 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: '',
},
],
},
],
},
],
},
// ----------------------------------
// 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: 'Simple',
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.<br>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',
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" target="_blank">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" target="_blank">View details</a>',
},
],
},
// ----------------------------------
// Get All
// ----------------------------------
{
displayName: 'Key Condition Expression',
name: 'keyConditionExpression',
description: 'Condition to determine the items to be retrieved. The condition must perform an equality test<br>on a single partition key value, in this format: <code>partitionKeyName = :partitionkeyval</code>',
placeholder: 'id = :id',
default: '',
type: 'string',
required: true,
displayOptions: {
show: {
resource: [
'item',
],
operation: [
'getAll',
],
},
},
},
{
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: 'How many 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: 'Simple',
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 <br>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.<br>These attributes can include scalars, sets, or elements of a JSON document. The attributes<br>in the expression must be separated by commas',
},
{
displayName: 'Filter Expression',
name: 'filterExpression',
type: 'string',
default: '',
description: 'Text that contains conditions that DynamoDB applies after the Query operation,<br>but before the data is returned. Items that do not satisfy the FilterExpression criteria</br>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 target="_blank" href="https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_PutItem.html">Info</a>',
},
],
},
] as INodeProperties[];

View file

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg viewBox="-40 -35 340 340" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid">
<g>
<path d="M165.258,288.501 L168.766,288.501 L226.027,259.867 L226.98,258.52 L226.98,29.964 L226.027,28.61 L168.766,0 L165.215,0 L165.258,288.501" fill="#5294CF"/>
<path d="M90.741,288.501 L87.184,288.501 L29.972,259.867 L28.811,257.87 L28.222,31.128 L29.972,28.61 L87.184,0 L90.785,0 L90.741,288.501" fill="#1F5B98"/>
<path d="M87.285,0 L168.711,0 L168.711,288.501 L87.285,288.501 L87.285,0 Z" fill="#2D72B8"/>
<path d="M256,137.769 L254.065,137.34 L226.437,134.764 L226.027,134.968 L168.715,132.676 L87.285,132.676 L29.972,134.968 L29.972,91.264 L29.912,91.296 L29.972,91.168 L87.285,77.888 L168.715,77.888 L226.027,91.168 L247.096,102.367 L247.096,95.167 L256,94.193 L255.078,92.395 L226.886,72.236 L226.027,72.515 L168.715,54.756 L87.285,54.756 L29.972,72.515 L29.972,28.61 L0,63.723 L0,94.389 L0.232,94.221 L8.904,95.167 L8.904,102.515 L0,107.28 L0,137.793 L0.232,137.769 L8.904,137.897 L8.904,150.704 L1.422,150.816 L0,150.68 L0,181.205 L8.904,185.993 L8.904,193.426 L0.373,194.368 L0,194.088 L0,224.749 L29.972,259.867 L29.972,215.966 L87.285,233.725 L168.715,233.725 L226.196,215.914 L226.96,216.249 L254.781,196.387 L256,194.408 L247.096,193.426 L247.096,186.142 L245.929,185.676 L226.886,195.941 L226.196,197.381 L168.715,210.584 L168.715,210.6 L87.285,210.6 L87.285,210.584 L29.972,197.325 L29.972,153.461 L87.285,155.745 L87.285,155.801 L168.715,155.801 L226.027,153.461 L227.332,154.061 L254.111,151.755 L256,150.832 L247.096,150.704 L247.096,137.897 L256,137.769" fill="#1A476F"/>
<path d="M226.027,215.966 L226.027,259.867 L256,224.749 L256,194.288 L226.2,215.914 L226.027,215.966" fill="#2D72B8"/>
<path d="M226.027,197.421 L226.2,197.381 L256,181.353 L256,150.704 L226.027,153.461 L226.027,197.421" fill="#2D72B8"/>
<path d="M226.2,91.208 L226.027,91.168 L226.027,134.968 L256,137.769 L256,107.135 L226.2,91.208" fill="#2D72B8"/>
<path d="M226.2,72.687 L256,94.193 L256,63.731 L226.027,28.61 L226.027,72.515 L226.2,72.575 L226.2,72.687" fill="#2D72B8"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View file

@ -0,0 +1,79 @@
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;
}
interface IAttributeValueValue {
[type: string]: string | string[] | IAttributeValue[];
}
export interface IAttributeValueUi {
attribute: string;
type: AttributeValueType;
value: string;
}
export interface IAttributeNameUi {
key: string;
value: string;
}
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 enum EAttributeValueType {
S = 'S', SS = 'SS', M = 'M', L = 'L', NS = 'NS', N = 'N', BOOL = 'BOOL', B = 'B', BS = 'BS', NULL = 'NULL',
}
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
}
};

View file

@ -0,0 +1,134 @@
import {
IDataObject,
INodeExecutionData,
} from 'n8n-workflow';
import {
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[]) {
// tslint:disable-next-line: no-any
const ean: { [key: string]: any } = {};
eanUi.forEach(({ key, value }) => {
ean[addPound(key)] = { value } as IAttributeValueValue;
});
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';
// @ts-ignore
} else if (isNaN(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];
output[attribute] = decodeAttribute(type, content);
}
return output;
}
function decodeAttribute(type: AttributeValueType, attribute: string) {
switch (type) {
case 'BOOL':
return Boolean(attribute);
case 'N':
return Number(attribute);
case 'S':
return String(attribute);
case 'SS':
case 'NS':
return attribute;
default:
return null;
}
}
// tslint:disable-next-line: no-any
export function validateJSON(input: any): object {
try {
return JSON.parse(input);
} catch (error) {
throw new Error('Items must be a valid JSON');
}
}
export function copyInputItem(item: INodeExecutionData, properties: string[]): IDataObject {
// Prepare the data to insert and copy it to be returned
let newItem: IDataObject;
newItem = {};
for (const property of properties) {
if (item.json[property] === undefined) {
newItem[property] = null;
} else {
newItem[property] = JSON.parse(JSON.stringify(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;
}

View file

@ -305,6 +305,7 @@
"dist/nodes/Autopilot/AutopilotTrigger.node.js", "dist/nodes/Autopilot/AutopilotTrigger.node.js",
"dist/nodes/Aws/AwsLambda.node.js", "dist/nodes/Aws/AwsLambda.node.js",
"dist/nodes/Aws/Comprehend/AwsComprehend.node.js", "dist/nodes/Aws/Comprehend/AwsComprehend.node.js",
"dist/nodes/Aws/DynamoDB/AwsDynamoDB.node.js",
"dist/nodes/Aws/Rekognition/AwsRekognition.node.js", "dist/nodes/Aws/Rekognition/AwsRekognition.node.js",
"dist/nodes/Aws/S3/AwsS3.node.js", "dist/nodes/Aws/S3/AwsS3.node.js",
"dist/nodes/Aws/SES/AwsSes.node.js", "dist/nodes/Aws/SES/AwsSes.node.js",