2019-07-07 10:00:05 -07:00
import {
IExecuteFunctions ,
} from 'n8n-core' ;
2020-11-24 01:53:39 -08:00
2019-07-07 10:00:05 -07:00
import {
IDataObject ,
INodeExecutionData ,
INodeType ,
2020-10-01 05:01:39 -07:00
INodeTypeDescription ,
2019-07-07 10:00:05 -07:00
} from 'n8n-workflow' ;
import {
apiRequest ,
apiRequestAllItems ,
2020-11-24 01:53:39 -08:00
downloadRecordAttachments ,
2019-07-07 10:00:05 -07:00
} from './GenericFunctions' ;
export class Airtable implements INodeType {
description : INodeTypeDescription = {
displayName : 'Airtable' ,
name : 'airtable' ,
2020-11-24 01:53:39 -08:00
icon : 'file:airtable.svg' ,
2019-07-07 10:00:05 -07:00
group : [ 'input' ] ,
version : 1 ,
2019-08-26 13:05:20 -07:00
description : 'Read, update, write and delete data from Airtable' ,
2019-07-07 10:00:05 -07:00
defaults : {
name : 'Airtable' ,
2020-11-24 01:53:39 -08:00
color : '#000000' ,
2019-07-07 10:00:05 -07:00
} ,
inputs : [ 'main' ] ,
outputs : [ 'main' ] ,
credentials : [
{
name : 'airtableApi' ,
required : true ,
2020-10-22 06:46:03 -07:00
} ,
2019-07-07 10:00:05 -07:00
] ,
properties : [
{
displayName : 'Operation' ,
name : 'operation' ,
type : 'options' ,
options : [
{
name : 'Append' ,
value : 'append' ,
2020-07-24 03:56:41 -07:00
description : 'Append the data to a table' ,
2019-07-07 10:00:05 -07:00
} ,
{
name : 'Delete' ,
value : 'delete' ,
2020-10-22 06:46:03 -07:00
description : 'Delete data from a table' ,
2019-07-07 10:00:05 -07:00
} ,
{
name : 'List' ,
value : 'list' ,
2020-10-22 06:46:03 -07:00
description : 'List data from a table' ,
2019-07-07 10:00:05 -07:00
} ,
{
name : 'Read' ,
value : 'read' ,
2020-10-22 06:46:03 -07:00
description : 'Read data from a table' ,
2019-07-07 10:00:05 -07:00
} ,
{
name : 'Update' ,
value : 'update' ,
2020-10-22 06:46:03 -07:00
description : 'Update data in a table' ,
2019-07-07 10:00:05 -07:00
} ,
] ,
default : 'read' ,
description : 'The operation to perform.' ,
} ,
// ----------------------------------
// All
// ----------------------------------
{
2020-11-10 13:40:44 -08:00
displayName : 'Base ID' ,
2019-07-07 10:00:05 -07:00
name : 'application' ,
2020-02-10 17:39:44 -08:00
type : 'string' ,
2019-07-07 10:00:05 -07:00
default : '' ,
required : true ,
2020-11-10 13:40:44 -08:00
description : 'The ID of the base to access.' ,
2019-07-07 10:00:05 -07:00
} ,
{
displayName : 'Table' ,
name : 'table' ,
type : 'string' ,
default : '' ,
placeholder : 'Stories' ,
required : true ,
2020-02-10 17:39:44 -08:00
description : 'The name of table to access.' ,
2019-07-07 10:00:05 -07:00
} ,
// ----------------------------------
// append
// ----------------------------------
{
displayName : 'Add All Fields' ,
name : 'addAllFields' ,
type : 'boolean' ,
displayOptions : {
show : {
operation : [
'append' ,
] ,
} ,
} ,
default : true ,
2020-09-17 22:58:49 -07:00
description : 'If all fields should be sent to Airtable or only specific ones.' ,
2019-07-07 10:00:05 -07:00
} ,
{
displayName : 'Fields' ,
name : 'fields' ,
type : 'string' ,
typeOptions : {
multipleValues : true ,
multipleValueButtonText : 'Add Field' ,
} ,
displayOptions : {
show : {
addAllFields : [
false ,
] ,
operation : [
'append' ,
] ,
} ,
} ,
2019-10-20 08:38:11 -07:00
default : [ ] ,
2019-07-07 10:00:05 -07:00
placeholder : 'Name' ,
required : true ,
2020-09-17 22:58:49 -07:00
description : 'The name of fields for which data should be sent to Airtable.' ,
2019-07-07 10:00:05 -07:00
} ,
// ----------------------------------
// delete
// ----------------------------------
{
displayName : 'Id' ,
name : 'id' ,
type : 'string' ,
displayOptions : {
show : {
operation : [
'delete' ,
] ,
} ,
} ,
default : '' ,
required : true ,
description : 'Id of the record to delete.' ,
} ,
// ----------------------------------
// list
// ----------------------------------
{
displayName : 'Return All' ,
name : 'returnAll' ,
type : 'boolean' ,
displayOptions : {
show : {
operation : [
'list' ,
] ,
} ,
} ,
default : true ,
description : 'If all results should be returned or only up to a given limit.' ,
} ,
{
displayName : 'Limit' ,
name : 'limit' ,
type : 'number' ,
displayOptions : {
show : {
operation : [
'list' ,
] ,
returnAll : [
false ,
] ,
} ,
} ,
typeOptions : {
minValue : 1 ,
maxValue : 100 ,
} ,
default : 100 ,
2020-09-17 22:58:49 -07:00
description : 'Number of results to return.' ,
2019-07-07 10:00:05 -07:00
} ,
2020-11-24 01:53:39 -08:00
{
displayName : 'Download Attachments' ,
name : 'downloadAttachments' ,
type : 'boolean' ,
displayOptions : {
show : {
operation : [
'list' ,
] ,
} ,
} ,
default : false ,
description : ` When set to true the attachment fields define in 'Download Fields' will be downloaded. ` ,
} ,
{
displayName : 'Download Fields' ,
name : 'downloadFieldNames' ,
type : 'string' ,
required : true ,
displayOptions : {
show : {
operation : [
'list' ,
] ,
downloadAttachments : [
true ,
] ,
} ,
} ,
default : '' ,
description : ` Name of the fields of type 'attachment' that should be downloaded. Multiple ones can be defined separated by comma. Case sensitive. ` ,
} ,
2019-07-07 10:00:05 -07:00
{
displayName : 'Additional Options' ,
name : 'additionalOptions' ,
type : 'collection' ,
displayOptions : {
show : {
operation : [
2020-10-22 06:46:03 -07:00
'list' ,
2019-07-07 10:00:05 -07:00
] ,
} ,
} ,
default : { } ,
description : 'Additional options which decide which records should be returned' ,
placeholder : 'Add Option' ,
options : [
{
displayName : 'Fields' ,
name : 'fields' ,
type : 'string' ,
typeOptions : {
multipleValues : true ,
multipleValueButtonText : 'Add Field' ,
} ,
2019-10-20 08:38:11 -07:00
default : [ ] ,
2019-07-07 10:00:05 -07:00
placeholder : 'Name' ,
description : 'Only data for fields whose names are in this list will be included in the records.' ,
} ,
{
displayName : 'Filter By Formula' ,
name : 'filterByFormula' ,
type : 'string' ,
default : '' ,
placeholder : 'NOT({Name} = \'\')' ,
description : 'A formula used to filter records. The formula will be evaluated for each<br />record, and if the result is not 0, false, "", NaN, [], or #Error!<br />the record will be included in the response.' ,
} ,
{
displayName : 'Sort' ,
name : 'sort' ,
placeholder : 'Add Sort Rule' ,
description : 'Defines how the returned records should be ordered.' ,
type : 'fixedCollection' ,
typeOptions : {
multipleValues : true ,
} ,
default : { } ,
options : [
{
name : 'property' ,
displayName : 'Property' ,
values : [
{
displayName : 'Field' ,
name : 'field' ,
type : 'string' ,
default : '' ,
description : 'Name of the field to sort on.' ,
} ,
{
displayName : 'Direction' ,
name : 'direction' ,
type : 'options' ,
options : [
{
name : 'ASC' ,
value : 'asc' ,
description : 'Sort in ascending order (small -> large)' ,
} ,
{
name : 'DESC' ,
value : 'desc' ,
2020-10-22 06:46:03 -07:00
description : 'Sort in descending order (large -> small)' ,
2019-07-07 10:00:05 -07:00
} ,
] ,
default : 'asc' ,
description : 'The sort direction.' ,
} ,
2020-10-22 06:46:03 -07:00
] ,
2019-07-07 10:00:05 -07:00
} ,
] ,
} ,
{
displayName : 'View' ,
name : 'view' ,
type : 'string' ,
default : '' ,
placeholder : 'All Stories' ,
description : 'The name or ID of a view in the Stories table. If set,<br />only the records in that view will be returned. The records<br />will be sorted according to the order of the view.' ,
} ,
] ,
} ,
// ----------------------------------
// read
// ----------------------------------
{
displayName : 'Id' ,
name : 'id' ,
type : 'string' ,
displayOptions : {
show : {
operation : [
'read' ,
] ,
} ,
} ,
default : '' ,
required : true ,
description : 'Id of the record to return.' ,
} ,
// ----------------------------------
// update
// ----------------------------------
{
displayName : 'Id' ,
name : 'id' ,
type : 'string' ,
displayOptions : {
show : {
operation : [
'update' ,
] ,
} ,
} ,
default : '' ,
required : true ,
description : 'Id of the record to update.' ,
} ,
{
displayName : 'Update All Fields' ,
name : 'updateAllFields' ,
type : 'boolean' ,
displayOptions : {
show : {
operation : [
'update' ,
] ,
} ,
} ,
default : true ,
2020-09-17 22:58:49 -07:00
description : 'If all fields should be sent to Airtable or only specific ones.' ,
2019-07-07 10:00:05 -07:00
} ,
{
displayName : 'Fields' ,
name : 'fields' ,
type : 'string' ,
typeOptions : {
multipleValues : true ,
multipleValueButtonText : 'Add Field' ,
} ,
displayOptions : {
show : {
updateAllFields : [
false ,
] ,
operation : [
'update' ,
] ,
} ,
} ,
2019-10-20 08:38:11 -07:00
default : [ ] ,
2019-07-07 10:00:05 -07:00
placeholder : 'Name' ,
required : true ,
2020-09-17 22:58:49 -07:00
description : 'The name of fields for which data should be sent to Airtable.' ,
2019-07-07 10:00:05 -07:00
} ,
2020-09-17 22:58:49 -07:00
// ----------------------------------
// append + update
// ----------------------------------
{
2020-09-17 22:59:10 -07:00
displayName : 'Options' ,
name : 'options' ,
type : 'collection' ,
placeholder : 'Add Option' ,
2020-09-17 22:58:49 -07:00
displayOptions : {
show : {
operation : [
'append' ,
'update' ,
] ,
} ,
} ,
2020-09-17 22:59:10 -07:00
default : { } ,
options : [
2020-10-22 14:36:30 -07:00
{
displayName : 'Ignore Fields' ,
name : 'ignoreFields' ,
type : 'string' ,
displayOptions : {
show : {
'/operation' : [
'update' ,
] ,
'/updateAllFields' : [
true ,
] ,
} ,
} ,
default : '' ,
description : 'Comma separated list of fields to ignore.' ,
} ,
2020-09-17 22:59:10 -07:00
{
displayName : 'Typecast' ,
name : 'typecast' ,
type : 'boolean' ,
default : false ,
description : 'If the Airtable API should attempt mapping of string values for linked records & select options.' ,
} ,
] ,
2020-09-17 22:58:49 -07:00
} ,
2019-07-07 10:00:05 -07:00
] ,
} ;
async execute ( this : IExecuteFunctions ) : Promise < INodeExecutionData [ ] [ ] > {
const items = this . getInputData ( ) ;
const returnData : IDataObject [ ] = [ ] ;
let responseData ;
const operation = this . getNodeParameter ( 'operation' , 0 ) as string ;
const application = this . getNodeParameter ( 'application' , 0 ) as string ;
2020-08-24 17:55:57 -07:00
const table = encodeURI ( this . getNodeParameter ( 'table' , 0 ) as string ) ;
2019-07-07 10:00:05 -07:00
let returnAll = false ;
let endpoint = '' ;
let requestMethod = '' ;
const body : IDataObject = { } ;
const qs : IDataObject = { } ;
if ( operation === 'append' ) {
// ----------------------------------
// append
// ----------------------------------
requestMethod = 'POST' ;
endpoint = ` ${ application } / ${ table } ` ;
let addAllFields : boolean ;
let fields : string [ ] ;
2020-09-17 22:59:10 -07:00
let options : IDataObject ;
2019-07-07 10:00:05 -07:00
for ( let i = 0 ; i < items . length ; i ++ ) {
addAllFields = this . getNodeParameter ( 'addAllFields' , i ) as boolean ;
2020-09-17 22:59:10 -07:00
options = this . getNodeParameter ( 'options' , i , { } ) as IDataObject ;
2019-07-07 10:00:05 -07:00
if ( addAllFields === true ) {
// Add all the fields the item has
body . fields = items [ i ] . json ;
} else {
// Add only the specified fields
body . fields = { } as IDataObject ;
fields = this . getNodeParameter ( 'fields' , i , [ ] ) as string [ ] ;
for ( const fieldName of fields ) {
// @ts-ignore
body . fields [ fieldName ] = items [ i ] . json [ fieldName ] ;
}
}
2020-09-17 22:59:10 -07:00
if ( options . typecast === true ) {
2020-09-17 22:58:49 -07:00
body [ 'typecast' ] = true ;
}
2019-07-07 10:00:05 -07:00
responseData = await apiRequest . call ( this , requestMethod , endpoint , body , qs ) ;
returnData . push ( responseData ) ;
}
} else if ( operation === 'delete' ) {
requestMethod = 'DELETE' ;
let id : string ;
for ( let i = 0 ; i < items . length ; i ++ ) {
id = this . getNodeParameter ( 'id' , i ) as string ;
endpoint = ` ${ application } / ${ table } / ${ id } ` ;
// Make one request after another. This is slower but makes
// sure that we do not run into the rate limit they have in
// place and so block for 30 seconds. Later some global
// functionality in core should make it easy to make requests
// according to specific rules like not more than 5 requests
// per seconds.
responseData = await apiRequest . call ( this , requestMethod , endpoint , body , qs ) ;
returnData . push ( responseData ) ;
}
} else if ( operation === 'list' ) {
// ----------------------------------
// list
// ----------------------------------
requestMethod = 'GET' ;
endpoint = ` ${ application } / ${ table } ` ;
returnAll = this . getNodeParameter ( 'returnAll' , 0 ) as boolean ;
2020-11-24 01:53:39 -08:00
const downloadAttachments = this . getNodeParameter ( 'downloadAttachments' , 0 ) as boolean ;
2019-07-07 10:00:05 -07:00
const additionalOptions = this . getNodeParameter ( 'additionalOptions' , 0 , { } ) as IDataObject ;
for ( const key of Object . keys ( additionalOptions ) ) {
if ( key === 'sort' && ( additionalOptions . sort as IDataObject ) . property !== undefined ) {
qs [ key ] = ( additionalOptions [ key ] as IDataObject ) . property ;
} else {
qs [ key ] = additionalOptions [ key ] ;
}
}
if ( returnAll === true ) {
responseData = await apiRequestAllItems . call ( this , requestMethod , endpoint , body , qs ) ;
} else {
qs . maxRecords = this . getNodeParameter ( 'limit' , 0 ) as number ;
responseData = await apiRequest . call ( this , requestMethod , endpoint , body , qs ) ;
}
returnData . push . apply ( returnData , responseData . records ) ;
2020-11-24 01:53:39 -08:00
if ( downloadAttachments === true ) {
const downloadFieldNames = ( this . getNodeParameter ( 'downloadFieldNames' , 0 ) as string ) . split ( ',' ) ;
const data = await downloadRecordAttachments . call ( this , responseData . records , downloadFieldNames ) ;
return [ data ] ;
}
2019-07-07 10:00:05 -07:00
} else if ( operation === 'read' ) {
// ----------------------------------
// read
// ----------------------------------
requestMethod = 'GET' ;
let id : string ;
for ( let i = 0 ; i < items . length ; i ++ ) {
id = this . getNodeParameter ( 'id' , i ) as string ;
endpoint = ` ${ application } / ${ table } / ${ id } ` ;
// Make one request after another. This is slower but makes
// sure that we do not run into the rate limit they have in
// place and so block for 30 seconds. Later some global
// functionality in core should make it easy to make requests
// according to specific rules like not more than 5 requests
// per seconds.
responseData = await apiRequest . call ( this , requestMethod , endpoint , body , qs ) ;
returnData . push ( responseData ) ;
}
} else if ( operation === 'update' ) {
// ----------------------------------
// update
// ----------------------------------
requestMethod = 'PATCH' ;
let id : string ;
let updateAllFields : boolean ;
let fields : string [ ] ;
2020-09-17 22:59:10 -07:00
let options : IDataObject ;
2019-07-07 10:00:05 -07:00
for ( let i = 0 ; i < items . length ; i ++ ) {
updateAllFields = this . getNodeParameter ( 'updateAllFields' , i ) as boolean ;
2020-09-17 22:59:10 -07:00
options = this . getNodeParameter ( 'options' , i , { } ) as IDataObject ;
2019-07-07 10:00:05 -07:00
if ( updateAllFields === true ) {
// Update all the fields the item has
body . fields = items [ i ] . json ;
2020-10-22 14:36:30 -07:00
if ( options . ignoreFields && options . ignoreFields !== '' ) {
const ignoreFields = ( options . ignoreFields as string ) . split ( ',' ) . map ( field = > field . trim ( ) ) . filter ( field = > ! ! field ) ;
if ( ignoreFields . length ) {
// From: https://stackoverflow.com/questions/17781472/how-to-get-a-subset-of-a-javascript-objects-properties
body . fields = Object . entries ( items [ i ] . json )
. filter ( ( [ key ] ) = > ! ignoreFields . includes ( key ) )
. reduce ( ( obj , [ key , val ] ) = > Object . assign ( obj , { [ key ] : val } ) , { } ) ;
}
}
2019-07-07 10:00:05 -07:00
} else {
// Update only the specified fields
body . fields = { } as IDataObject ;
fields = this . getNodeParameter ( 'fields' , i , [ ] ) as string [ ] ;
for ( const fieldName of fields ) {
// @ts-ignore
body . fields [ fieldName ] = items [ i ] . json [ fieldName ] ;
}
}
2020-09-17 22:59:10 -07:00
if ( options . typecast === true ) {
2020-09-17 22:58:49 -07:00
body [ 'typecast' ] = true ;
}
2019-07-07 10:00:05 -07:00
id = this . getNodeParameter ( 'id' , i ) as string ;
endpoint = ` ${ application } / ${ table } / ${ id } ` ;
// Make one request after another. This is slower but makes
// sure that we do not run into the rate limit they have in
// place and so block for 30 seconds. Later some global
// functionality in core should make it easy to make requests
// according to specific rules like not more than 5 requests
// per seconds.
responseData = await apiRequest . call ( this , requestMethod , endpoint , body , qs ) ;
returnData . push ( responseData ) ;
}
} else {
throw new Error ( ` The operation " ${ operation } " is not known! ` ) ;
}
return [ this . helpers . returnJsonArray ( returnData ) ] ;
}
}