From b058aee6c1c554d97f2f49a83b6c2cb269f78391 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Mon, 12 Jul 2021 13:26:21 +0200 Subject: [PATCH] :sparkles: Add Baserow node (#1938) * Add Baserow node * :zap: Add JWT method to credentials * :zap: Refactor Baserow node * :hammer: Refactor to add continueOnFail * :zap: Extract table ID from URL * :pencil2: Reword descriptions per feedback * :fire: Remove API token auth per feedback * :hammer: Reformat for readability * :bulb: Fix issues identified by nodelinter * :zap: Add columns param to create and update * :zap: Refactor JWT token retrieval * :zap: Add resource loaders * :zap: Improvements * :zap: Improve types * :zap: Clean up descriptions and comments * :zap: Make minor UX improvements * :zap: Update input data description * :hammer: Refactor data to send for create and update * :zap: Add text to description * :zap: Small improvements * :zap: Change parameter names and descriptions Co-authored-by: Jeremie Pardou-Piquemal <571533+jrmi@users.noreply.github.com> Co-authored-by: ricardo Co-authored-by: Jan Oberhauser --- .../credentials/BaserowApi.credentials.ts | 34 ++ .../nodes/Baserow/Baserow.node.json | 19 + .../nodes-base/nodes/Baserow/Baserow.node.ts | 318 ++++++++++++ .../nodes/Baserow/GenericFunctions.ts | 198 ++++++++ .../nodes/Baserow/OperationDescription.ts | 455 ++++++++++++++++++ packages/nodes-base/nodes/Baserow/baserow.svg | 143 ++++++ packages/nodes-base/nodes/Baserow/types.d.ts | 41 ++ packages/nodes-base/package.json | 2 + 8 files changed, 1210 insertions(+) create mode 100644 packages/nodes-base/credentials/BaserowApi.credentials.ts create mode 100644 packages/nodes-base/nodes/Baserow/Baserow.node.json create mode 100644 packages/nodes-base/nodes/Baserow/Baserow.node.ts create mode 100644 packages/nodes-base/nodes/Baserow/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/Baserow/OperationDescription.ts create mode 100644 packages/nodes-base/nodes/Baserow/baserow.svg create mode 100644 packages/nodes-base/nodes/Baserow/types.d.ts diff --git a/packages/nodes-base/credentials/BaserowApi.credentials.ts b/packages/nodes-base/credentials/BaserowApi.credentials.ts new file mode 100644 index 0000000000..ff81c05fee --- /dev/null +++ b/packages/nodes-base/credentials/BaserowApi.credentials.ts @@ -0,0 +1,34 @@ +import { + ICredentialType, + INodeProperties, +} from 'n8n-workflow'; + +// https://api.baserow.io/api/redoc/#section/Authentication + +export class BaserowApi implements ICredentialType { + name = 'baserowApi'; + displayName = 'Baserow API'; + properties: INodeProperties[] = [ + { + displayName: 'Host', + name: 'host', + type: 'string', + default: 'https://api.baserow.io', + }, + { + displayName: 'Username', + name: 'username', + type: 'string', + default: '', + }, + { + displayName: 'Password', + name: 'password', + type: 'string', + default: '', + typeOptions: { + password: true, + }, + }, + ]; +} diff --git a/packages/nodes-base/nodes/Baserow/Baserow.node.json b/packages/nodes-base/nodes/Baserow/Baserow.node.json new file mode 100644 index 0000000000..3ab9005110 --- /dev/null +++ b/packages/nodes-base/nodes/Baserow/Baserow.node.json @@ -0,0 +1,19 @@ +{ + "node": "n8n-nodes-base.baserow", + "nodeVersion": "1.0", + "codexVersion": "1.0", + "categories": ["Data & Storage"], + "resources": { + "credentialDocumentation": [ + { + "url": "https://docs.n8n.io/credentials/baserow" + } + ], + "primaryDocumentation": [ + { + "url": "https://docs.n8n.io/nodes/n8n-nodes-base.baserow/" + } + ], + "generic": [] + } +} diff --git a/packages/nodes-base/nodes/Baserow/Baserow.node.ts b/packages/nodes-base/nodes/Baserow/Baserow.node.ts new file mode 100644 index 0000000000..c3a26bc580 --- /dev/null +++ b/packages/nodes-base/nodes/Baserow/Baserow.node.ts @@ -0,0 +1,318 @@ +import { + IDataObject, + ILoadOptionsFunctions, + INodeExecutionData, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; + +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + baserowApiRequest, + baserowApiRequestAllItems, + getJwtToken, + TableFieldMapper, + toOptions, +} from './GenericFunctions'; + +import { + operationFields +} from './OperationDescription'; + +import { + BaserowCredentials, + FieldsUiValues, + GetAllAdditionalOptions, + LoadedResource, + Operation, + Row, +} from './types'; + +export class Baserow implements INodeType { + description: INodeTypeDescription = { + displayName: 'Baserow', + name: 'baserow', + icon: 'file:baserow.svg', + group: ['output'], + version: 1, + description: 'Consume the Baserow API', + subtitle: '={{$parameter["operation"] + ":" + $parameter["resource"]}}', + defaults: { + name: 'Baserow', + color: '#00a2ce', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'baserowApi', + required: true, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Row', + value: 'row', + }, + ], + default: 'row', + description: 'Operation to perform', + }, + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'row', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a row', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a row', + }, + { + name: 'Get', + value: 'get', + description: 'Retrieve a row', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Retrieve all rows', + }, + { + name: 'Update', + value: 'update', + description: 'Update a row', + }, + ], + default: 'getAll', + description: 'Operation to perform', + }, + ...operationFields, + ], + }; + + methods = { + loadOptions: { + async getDatabaseIds(this: ILoadOptionsFunctions) { + const credentials = this.getCredentials('baserowApi') as BaserowCredentials; + const jwtToken = await getJwtToken.call(this, credentials); + const endpoint = '/api/applications/'; + const databases = await baserowApiRequest.call(this, 'GET', endpoint, {}, {}, jwtToken) as LoadedResource[]; + return toOptions(databases); + }, + + async getTableIds(this: ILoadOptionsFunctions) { + const credentials = this.getCredentials('baserowApi') as BaserowCredentials; + const jwtToken = await getJwtToken.call(this, credentials); + const databaseId = this.getNodeParameter('databaseId', 0) as string; + const endpoint = `/api/database/tables/database/${databaseId}`; + const tables = await baserowApiRequest.call(this, 'GET', endpoint, {}, {}, jwtToken) as LoadedResource[]; + return toOptions(tables); + }, + + async getTableFields(this: ILoadOptionsFunctions) { + const credentials = this.getCredentials('baserowApi') as BaserowCredentials; + const jwtToken = await getJwtToken.call(this, credentials); + const tableId = this.getNodeParameter('tableId', 0) as string; + const endpoint = `/api/database/fields/table/${tableId}/`; + const fields = await baserowApiRequest.call(this, 'GET', endpoint, {}, {}, jwtToken) as LoadedResource[]; + return toOptions(fields); + }, + }, + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const mapper = new TableFieldMapper(); + const returnData: IDataObject[] = []; + const operation = this.getNodeParameter('operation', 0) as Operation; + + const tableId = this.getNodeParameter('tableId', 0) as string; + const credentials = this.getCredentials('baserowApi') as BaserowCredentials; + const jwtToken = await getJwtToken.call(this, credentials); + const fields = await mapper.getTableFields.call(this, tableId, jwtToken); + mapper.createMappings(fields); + + for (let i = 0; i < items.length; i++) { + + try { + + if (operation === 'getAll') { + + // ---------------------------------- + // getAll + // ---------------------------------- + + // https://api.baserow.io/api/redoc/#operation/list_database_table_rows + + const { order, filters, filterType, search } = this.getNodeParameter('additionalOptions', 0) as GetAllAdditionalOptions; + + const qs: IDataObject = {}; + + if (order?.fields) { + qs['order_by'] = order.fields + .map(({ field, direction }) => `${direction}${mapper.setField(field)}`) + .join(','); + } + + if (filters?.fields) { + filters.fields.forEach(({ field, operator, value }) => { + qs[`filter__field_${mapper.setField(field)}__${operator}`] = value; + }); + } + + if (filterType) { + qs.filter_type = filterType; + } + + if (search) { + qs.search = search; + } + + const endpoint = `/api/database/rows/table/${tableId}/`; + const rows = await baserowApiRequestAllItems.call(this, 'GET', endpoint, {}, qs, jwtToken) as Row[]; + + rows.forEach(row => mapper.idsToNames(row)); + + returnData.push(...rows); + + } else if (operation === 'get') { + + // ---------------------------------- + // get + // ---------------------------------- + + // https://api.baserow.io/api/redoc/#operation/get_database_table_row + + const rowId = this.getNodeParameter('rowId', i) as string; + const endpoint = `/api/database/rows/table/${tableId}/${rowId}/`; + const row = await baserowApiRequest.call(this, 'GET', endpoint, {}, {}, jwtToken); + + mapper.idsToNames(row); + + returnData.push(row); + + } else if (operation === 'create') { + + // ---------------------------------- + // create + // ---------------------------------- + + // https://api.baserow.io/api/redoc/#operation/create_database_table_row + + const body: IDataObject = {}; + + const dataToSend = this.getNodeParameter('dataToSend', 0) as 'defineBelow' | 'autoMapColumns'; + + if (dataToSend === 'autoMapColumns') { + + const incomingKeys = Object.keys(items[i].json); + const rawInputsToIgnore = this.getNodeParameter('inputDataToIgnore', i) as string; + const inputDataToIgnore = rawInputsToIgnore.split(',').map(c => c.trim()); + + for (const key of incomingKeys) { + if (inputDataToIgnore.includes(key)) continue; + body[key] = items[i].json[key]; + mapper.namesToIds(body); + } + } else { + const fields = this.getNodeParameter('fieldsUi.fieldValues', i, []) as FieldsUiValues; + for (const field of fields) { + body[`field_${field.fieldId}`] = field.fieldValue; + } + } + + const endpoint = `/api/database/rows/table/${tableId}/`; + const createdRow = await baserowApiRequest.call(this, 'POST', endpoint, body, {}, jwtToken); + + mapper.idsToNames(createdRow); + + returnData.push(createdRow); + + } else if (operation === 'update') { + + // ---------------------------------- + // update + // ---------------------------------- + + // https://api.baserow.io/api/redoc/#operation/update_database_table_row + + const rowId = this.getNodeParameter('rowId', i) as string; + + const body: IDataObject = {}; + + const dataToSend = this.getNodeParameter('dataToSend', 0) as 'defineBelow' | 'autoMapInputData'; + + 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; + body[key] = items[i].json[key]; + mapper.namesToIds(body); + } + } else { + const fields = this.getNodeParameter('fieldsUi.fieldValues', i, []) as FieldsUiValues; + for (const field of fields) { + body[`field_${field.fieldId}`] = field.fieldValue; + } + } + + const endpoint = `/api/database/rows/table/${tableId}/${rowId}/`; + const updatedRow = await baserowApiRequest.call(this, 'PATCH', endpoint, body, {}, jwtToken); + + mapper.idsToNames(updatedRow); + + returnData.push(updatedRow); + + } else if (operation === 'delete') { + + // ---------------------------------- + // delete + // ---------------------------------- + + // https://api.baserow.io/api/redoc/#operation/delete_database_table_row + + const rowId = this.getNodeParameter('rowId', i) as string; + + const endpoint = `/api/database/rows/table/${tableId}/${rowId}/`; + await baserowApiRequest.call(this, 'DELETE', endpoint, {}, {}, jwtToken); + + returnData.push({ success: true }); + } + + } catch (error) { + if (this.continueOnFail()) { + returnData.push({ error: error.message }); + continue; + } + throw error; + } + } + + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/nodes/Baserow/GenericFunctions.ts b/packages/nodes-base/nodes/Baserow/GenericFunctions.ts new file mode 100644 index 0000000000..e360b6192b --- /dev/null +++ b/packages/nodes-base/nodes/Baserow/GenericFunctions.ts @@ -0,0 +1,198 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + OptionsWithUri, +} from 'request'; + +import { + IDataObject, + ILoadOptionsFunctions, + NodeApiError, + NodeOperationError, +} from 'n8n-workflow'; + +import { + Accumulator, + BaserowCredentials, + LoadedResource, +} from './types'; + +/** + * Make a request to Baserow API. + */ +export async function baserowApiRequest( + this: IExecuteFunctions | ILoadOptionsFunctions, + method: string, + endpoint: string, + body: IDataObject = {}, + qs: IDataObject = {}, + jwtToken: string, +) { + const credentials = this.getCredentials('baserowApi') as BaserowCredentials; + + if (credentials === undefined) { + throw new NodeOperationError(this.getNode(), 'No credentials got returned!'); + } + + const options: OptionsWithUri = { + headers: { + Authorization: `JWT ${jwtToken}`, + }, + method, + body, + qs, + uri: `${credentials.host}${endpoint}`, + json: true, + }; + + if (Object.keys(qs).length === 0) { + delete options.qs; + } + + if (Object.keys(body).length === 0) { + delete options.body; + } + + try { + return await this.helpers.request!(options); + } catch (error) { + throw new NodeApiError(this.getNode(), error); + } +} + +/** + * Get all results from a paginated query to Baserow API. + */ +export async function baserowApiRequestAllItems( + this: IExecuteFunctions, + method: string, + endpoint: string, + body: IDataObject, + qs: IDataObject = {}, + jwtToken: string, +): Promise { + const returnData: IDataObject[] = []; + let responseData; + + qs.page = 1; + qs.size = 100; + + const returnAll = this.getNodeParameter('returnAll', 0, false) as boolean; + const limit = this.getNodeParameter('limit', 0, 0) as number; + + do { + responseData = await baserowApiRequest.call(this, method, endpoint, body, qs, jwtToken); + returnData.push(...responseData.results); + + if (!returnAll && returnData.length > limit) { + return returnData.slice(0, limit); + } + + qs.page += 1; + } while (responseData.next !== null); + + return returnData; +} + +/** + * Get a JWT token based on Baserow account username and password. + */ +export async function getJwtToken( + this: IExecuteFunctions | ILoadOptionsFunctions, + { username, password, host }: BaserowCredentials, +) { + const options: OptionsWithUri = { + method: 'POST', + body: { + username, + password, + }, + uri: `${host}/api/user/token-auth/`, + json: true, + }; + + try { + const { token } = await this.helpers.request!(options) as { token: string }; + return token; + } catch (error) { + throw new NodeApiError(this.getNode(), error); + } +} + +export async function getFieldNamesAndIds( + this: IExecuteFunctions, + tableId: string, + jwtToken: string, +) { + const endpoint = `/api/database/fields/table/${tableId}/`; + const response = await baserowApiRequest.call(this, 'GET', endpoint, {}, {}, jwtToken) as LoadedResource[]; + + return { + names: response.map((field) => field.name), + ids: response.map((field) => `field_${field.id}`), + }; +} + +export const toOptions = (items: LoadedResource[]) => + items.map(({ name, id }) => ({ name, value: id })); + +/** + * Responsible for mapping field IDs `field_n` to names and vice versa. + */ +export class TableFieldMapper { + nameToIdMapping: Record = {}; + idToNameMapping: Record = {}; + mapIds = true; + + async getTableFields( + this: IExecuteFunctions, + table: string, + jwtToken: string, + ): Promise { + const endpoint = `/api/database/fields/table/${table}/`; + return await baserowApiRequest.call(this, 'GET', endpoint, {}, {}, jwtToken); + } + + createMappings(tableFields: LoadedResource[]) { + this.nameToIdMapping = this.createNameToIdMapping(tableFields); + this.idToNameMapping = this.createIdToNameMapping(tableFields); + } + + private createIdToNameMapping(responseData: LoadedResource[]) { + return responseData.reduce((acc, cur) => { + acc[`field_${cur.id}`] = cur.name; + return acc; + }, {}); + } + + private createNameToIdMapping(responseData: LoadedResource[]) { + return responseData.reduce((acc, cur) => { + acc[cur.name] = `field_${cur.id}`; + return acc; + }, {}); + } + + setField(field: string) { + return this.mapIds ? field : this.nameToIdMapping[field] ?? field; + } + + idsToNames(obj: Record) { + Object.entries(obj).forEach(([key, value]) => { + if (this.idToNameMapping[key] !== undefined) { + delete obj[key]; + obj[this.idToNameMapping[key]] = value; + } + }); + } + + namesToIds(obj: Record) { + Object.entries(obj).forEach(([key, value]) => { + if (this.nameToIdMapping[key] !== undefined) { + delete obj[key]; + obj[this.nameToIdMapping[key]] = value; + } + }); + } +} diff --git a/packages/nodes-base/nodes/Baserow/OperationDescription.ts b/packages/nodes-base/nodes/Baserow/OperationDescription.ts new file mode 100644 index 0000000000..76d20e0dab --- /dev/null +++ b/packages/nodes-base/nodes/Baserow/OperationDescription.ts @@ -0,0 +1,455 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const operationFields = [ + // ---------------------------------- + // shared + // ---------------------------------- + { + displayName: 'Database', + name: 'databaseId', + type: 'options', + default: '', + required: true, + description: 'Database to operate on', + typeOptions: { + loadOptionsMethod: 'getDatabaseIds', + }, + }, + { + displayName: 'Table', + name: 'tableId', + type: 'options', + default: '', + required: true, + description: 'Table to operate on', + typeOptions: { + loadOptionsDependsOn: [ + 'databaseId', + ], + loadOptionsMethod: 'getTableIds', + }, + }, + + // ---------------------------------- + // get + // ---------------------------------- + { + displayName: 'Row ID', + name: 'rowId', + type: 'string', + displayOptions: { + show: { + operation: [ + 'get', + ], + }, + }, + default: '', + required: true, + description: 'ID of the row to return', + }, + + // ---------------------------------- + // update + // ---------------------------------- + { + displayName: 'Row ID', + name: 'rowId', + type: 'string', + displayOptions: { + show: { + operation: [ + 'update', + ], + }, + }, + default: '', + required: true, + description: 'ID of the row to update', + }, + + // ---------------------------------- + // create/update + // ---------------------------------- + { + 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: [ + 'create', + 'update', + ], + }, + }, + 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: [ + 'create', + 'update', + ], + 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: [ + 'create', + 'update', + ], + dataToSend: [ + 'defineBelow', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Field', + name: 'fieldValues', + values: [ + { + displayName: 'Field ID', + name: 'fieldId', + type: 'options', + typeOptions: { + loadOptionsDependsOn: [ + 'tableId', + ], + loadOptionsMethod: 'getTableFields', + }, + default: '', + }, + { + displayName: 'Field Value', + name: 'fieldValue', + type: 'string', + default: '', + }, + ], + }, + ], + }, + + // ---------------------------------- + // delete + // ---------------------------------- + { + displayName: 'Row ID', + name: 'rowId', + type: 'string', + displayOptions: { + show: { + operation: [ + 'delete', + ], + }, + }, + default: '', + required: true, + description: 'ID of the row to delete', + }, + + // ---------------------------------- + // getAll + // ---------------------------------- + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + default: false, + description: 'Whether to return all results or only up to a given limit', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 50, + description: 'How many results to return', + typeOptions: { + minValue: 1, + maxValue: 100, + }, + displayOptions: { + show: { + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + }, + { + displayName: 'Options', + name: 'additionalOptions', + type: 'collection', + placeholder: 'Add Option', + default: {}, + displayOptions: { + show: { + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'Filters', + name: 'filters', + placeholder: 'Add Filter', + description: 'Filter rows based on comparison operators', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + default: {}, + options: [ + { + name: 'fields', + displayName: 'Field', + values: [ + { + displayName: 'Field', + name: 'field', + type: 'options', + default: '', + description: 'Field to compare', + typeOptions: { + loadOptionsDependsOn: [ + 'tableId', + ], + loadOptionsMethod: 'getTableFields', + }, + }, + { + displayName: 'Filter', + name: 'operator', + description: 'Operator to compare field and value with', + type: 'options', + options: [ + { + name: 'Equal', + value: 'equal', + description: 'Field is equal to value', + }, + { + name: 'Not Equal', + value: 'not_equal', + description: 'Field is not equal to value', + }, + { + name: 'Date Equal', + value: 'date_equal', + description: 'Field is date. Format: \'YYYY-MM-DD\'', + }, + { + name: 'Date Not Equal', + value: 'date_not_equal', + description: 'Field is not date. Format: \'YYYY-MM-DD\'', + }, + { + name: 'Date Equals Today', + value: 'date_equals_today', + description: 'Field is today. Format: string', + }, + { + name: 'Date Equals Month', + value: 'date_equals_month', + description: 'Field in this month. Format: string', + }, + { + name: 'Date Equals Year', + value: 'date_equals_year', + description: 'Field in this year. Format: string', + }, + { + name: 'Contains', + value: 'contains', + description: 'Field contains value', + }, + { + name: 'File Name Contains', + value: 'filename_contains', + description: 'Field filename contains value', + }, + { + name: 'Contains Not', + value: 'contains_not', + description: 'Field does not contain value', + }, + { + name: 'Higher Than', + value: 'higher_than', + description: 'Field is higher than value', + }, + { + name: 'Lower Than', + value: 'lower_than', + description: 'Field is lower than value', + }, + { + name: 'Single Select Equal', + value: 'single_select_equal', + description: 'Field selected option is value', + }, + { + name: 'Single Select Not Equal', + value: 'single_select_not_equal', + description: 'Field selected option is not value', + }, + { + name: 'Is True', + value: 'boolean', + description: 'Boolean field is true', + }, + { + name: 'Is Empty', + value: 'empty', + description: 'Field is empty', + }, + { + name: 'Not Empty', + value: 'not_empty', + description: 'Field is not empty', + }, + ], + default: 'equal', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + description: 'Value to compare to', + }, + ], + }, + ], + }, + { + displayName: 'Filter Type', + name: 'filterType', + type: 'options', + options: [ + { + name: 'AND', + value: 'AND', + description: 'Indicates that the rows must match all the provided filters', + }, + { + name: 'OR', + value: 'OR', + description: 'Indicates that the rows only have to match one of the filters', + }, + ], + default: 'AND', + description: 'This works only if two or more filters are provided. Defaults to AND', + }, + { + displayName: 'Search Term', + name: 'search', + type: 'string', + default: '', + description: 'Text to match (can be in any column)', + }, + { + displayName: 'Sorting', + name: 'order', + placeholder: 'Add Sort Order', + description: 'Set the sort order of the result rows', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + default: {}, + options: [ + { + name: 'Fields', + displayName: 'Field', + values: [ + { + displayName: 'Field Name', + name: 'field', + type: 'options', + default: '', + description: 'Field name to sort by', + typeOptions: { + loadOptionsDependsOn: [ + 'tableId', + ], + loadOptionsMethod: 'getTableFields', + }, + }, + { + displayName: 'Direction', + name: 'direction', + type: 'options', + options: [ + { + name: 'ASC', + value: '', + description: 'Sort in ascending order', + }, + { + name: 'DESC', + value: '-', + description: 'Sort in descending order', + }, + ], + default: '', + description: 'Sort direction, either ascending or descending', + }, + ], + }, + ], + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Baserow/baserow.svg b/packages/nodes-base/nodes/Baserow/baserow.svg new file mode 100644 index 0000000000..a184ad4b99 --- /dev/null +++ b/packages/nodes-base/nodes/Baserow/baserow.svg @@ -0,0 +1,143 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + +