diff --git a/packages/nodes-base/credentials/SupabaseApi.credentials.ts b/packages/nodes-base/credentials/SupabaseApi.credentials.ts new file mode 100644 index 0000000000000..9f9b5d957d656 --- /dev/null +++ b/packages/nodes-base/credentials/SupabaseApi.credentials.ts @@ -0,0 +1,25 @@ +import { + ICredentialType, + INodeProperties, +} from 'n8n-workflow'; + +export class SupabaseApi implements ICredentialType { + name = 'supabaseApi'; + displayName = 'Supabase API'; + documentationUrl = 'superbase'; + properties: INodeProperties[] = [ + { + displayName: 'Host', + name: 'host', + type: 'string', + placeholder: 'https://your_account.supabase.co', + default: '', + }, + { + displayName: 'Service Role Secret', + name: 'serviceRole', + type: 'string', + default: '', + }, + ]; +} diff --git a/packages/nodes-base/nodes/Supabase/GenericFunctions.ts b/packages/nodes-base/nodes/Supabase/GenericFunctions.ts new file mode 100644 index 0000000000000..870958cc81796 --- /dev/null +++ b/packages/nodes-base/nodes/Supabase/GenericFunctions.ts @@ -0,0 +1,319 @@ +import { + OptionsWithUri, +} from 'request'; + +import { + IExecuteFunctions, + IExecuteSingleFunctions, + IHookFunctions, + ILoadOptionsFunctions, + IWebhookFunctions, +} from 'n8n-core'; + +import { + ICredentialDataDecryptedObject, + ICredentialTestFunctions, + IDataObject, + INodeProperties, + NodeApiError, +} from 'n8n-workflow'; + +export async function supabaseApiRequest(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions | IHookFunctions | IWebhookFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, headers: IDataObject = {}): Promise { // tslint:disable-line:no-any + const credentials = await this.getCredentials('supabaseApi') as { host: string, serviceRole: string }; + + const options: OptionsWithUri = { + headers: { + apikey: credentials.serviceRole, + Prefer: 'return=representation', + }, + method, + qs, + body, + uri: uri || `${credentials.host}/rest/v1${resource}`, + json: true, + }; + try { + if (Object.keys(headers).length !== 0) { + options.headers = Object.assign({}, options.headers, headers); + } + if (Object.keys(body).length === 0) { + delete options.body; + } + //@ts-ignore + return await this.helpers?.request(options); + + } catch (error) { + throw new NodeApiError(this.getNode(), error); + } +} + +const mapOperations: { [key: string]: string } = { + 'create': 'created', + 'update': 'updated', + 'getAll': 'retrieved', + 'delete': 'deleted', +}; + +export function getFilters( + resources: string[], + operations: string[], + { + includeNoneOption = true, + filterTypeDisplayName = 'Filter', + filterFixedCollectionDisplayName = 'Filters', + filterStringDisplayName = 'Filters (String)', + mustMatchOptions = [ + { + name: 'Any Filter', + value: 'anyFilter', + }, + { + name: 'All Filters', + value: 'allFilters', + }, + ], + }): INodeProperties[] { + return [ + { + displayName: filterTypeDisplayName, + name: 'filterType', + type: 'options', + options: [ + ...(includeNoneOption ? [{ name: 'None', value: 'none' }] : []), + { + name: 'Build Manually', + value: 'manual', + }, + { + name: 'String', + value: 'string', + }, + ], + displayOptions: { + show: { + resource: resources, + operation: operations, + }, + }, + default: 'manual', + }, + { + displayName: 'Must Match', + name: 'matchType', + type: 'options', + options: mustMatchOptions, + displayOptions: { + show: { + resource: resources, + operation: operations, + filterType: [ + 'manual', + ], + }, + }, + default: 'anyFilter', + }, + { + displayName: filterFixedCollectionDisplayName, + name: 'filters', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + displayOptions: { + show: { + resource: resources, + operation: operations, + filterType: [ + 'manual', + ], + }, + }, + default: '', + placeholder: 'Add Condition', + options: [ + { + displayName: 'Conditions', + name: 'conditions', + values: [ + { + displayName: 'Field Name', + name: 'keyName', + type: 'options', + typeOptions: { + loadOptionsDependsOn: [ + 'tableId', + ], + loadOptionsMethod: 'getTableColumns', + }, + default: '', + }, + { + displayName: 'Condition', + name: 'condition', + type: 'options', + options: [ + { + name: 'Equals', + value: 'eq', + }, + { + name: 'Greater Than', + value: 'gt', + }, + { + name: 'Greater Than or Equal', + value: 'gte', + }, + { + name: 'Less Than', + value: 'lt', + }, + { + name: 'Less Than or Equal', + value: 'lte', + }, + { + name: 'Not Equals', + value: 'neq', + }, + { + name: 'LIKE operator', + value: 'like', + description: 'use * in place of %', + }, + { + name: 'ILIKE operator', + value: 'ilike', + description: 'use * in place of %', + }, + { + name: 'Is', + value: 'is', + description: 'Checking for exact equality (null,true,false,unknown)', + }, + { + name: 'Full-Text', + value: 'fullText', + }, + ], + default: '', + }, + { + displayName: 'Search Function', + name: 'searchFunction', + type: 'options', + displayOptions: { + show: { + condition: [ + 'fullText', + ], + }, + }, + options: [ + { + name: 'to_tsquery', + value: 'fts', + }, + { + name: 'plainto_tsquery', + value: 'plfts', + }, + { + name: 'phraseto_tsquery', + value: 'phfts', + }, + { + name: 'websearch_to_tsquery', + value: 'wfts', + }, + ], + default: '', + }, + { + displayName: 'Field Value', + name: 'keyValue', + type: 'string', + default: '', + }, + ], + }, + ], + description: `Filter to decide which rows get ${mapOperations[operations[0] as string]}`, + }, + { + displayName: 'See PostgREST guide to creating filters', + name: 'jsonNotice', + type: 'notice', + displayOptions: { + show: { + resource: resources, + operation: operations, + filterType: [ + 'string', + ], + }, + }, + default: '', + }, + { + displayName: 'Filters (String)', + name: 'filterString', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + displayOptions: { + show: { + resource: resources, + operation: operations, + filterType: [ + 'string', + ], + }, + }, + default: '', + placeholder: 'name=eq.jhon', + }, + ]; +} + +export const buildQuery = (obj: IDataObject, value: IDataObject) => { + if (value.condition === 'fullText') { + return Object.assign(obj, { [`${value.keyName}`]: `${value.searchFunction}.${value.keyValue}` }); + } + return Object.assign(obj, { [`${value.keyName}`]: `${value.condition}.${value.keyValue}` }); +}; + +export const buildOrQuery = (key: IDataObject) => { + if (key.condition === 'fullText') { + return `${key.keyName}.${key.searchFunction}.${key.keyValue}`; + } + return `${key.keyName}.${key.condition}.${key.keyValue}`; +}; + +export const buildGetQuery = (obj: IDataObject, value: IDataObject) => { + return Object.assign(obj, { [`${value.keyName}`]: `eq.${value.keyValue}` }); +}; + +export async function validateCrendentials( + this: ICredentialTestFunctions, + decryptedCredentials: ICredentialDataDecryptedObject): Promise { // tslint:disable-line:no-any + + const credentials = decryptedCredentials; + + const { serviceRole } = credentials as { + serviceRole: string, + }; + + const options: OptionsWithUri = { + headers: { + apikey: serviceRole, + }, + method: 'GET', + uri: `${credentials.host}/rest/v1/`, + json: true, + }; + + return this.helpers.request!(options); +} \ No newline at end of file diff --git a/packages/nodes-base/nodes/Supabase/RowDescription.ts b/packages/nodes-base/nodes/Supabase/RowDescription.ts new file mode 100644 index 0000000000000..c1d79e1b46348 --- /dev/null +++ b/packages/nodes-base/nodes/Supabase/RowDescription.ts @@ -0,0 +1,321 @@ + +import { + INodeProperties, +} from 'n8n-workflow'; + +import { + getFilters, +} from './GenericFunctions'; + +export const rowOperations: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'row', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a new row', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a row', + }, + { + name: 'Get', + value: 'get', + description: 'Get a row', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all rows', + }, + { + name: 'Update', + value: 'update', + description: 'Update a row', + }, + ], + default: 'create', + }, +]; + +export const rowFields: INodeProperties[] = [ + + /* -------------------------------------------------------------------------- */ + /* row:create */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Table Name', + name: 'tableId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getTables', + }, + required: true, + displayOptions: { + show: { + resource: [ + 'row', + ], + operation: [ + 'create', + 'delete', + 'get', + 'getAll', + 'update', + ], + }, + }, + default: '', + }, + ...getFilters( + ['row'], + ['update'], + { + includeNoneOption: false, + filterTypeDisplayName: 'Select Type', + filterStringDisplayName: 'Select Condition (String)', + filterFixedCollectionDisplayName: 'Select Conditions', + mustMatchOptions: [ + { + name: 'Any Select Condition', + value: 'anyFilter', + }, + { + name: 'All Select Conditions', + value: 'allFilters', + }, + ], + }), + { + 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: { + resource: [ + 'row', + ], + operation: [ + 'create', + 'update', + ], + }, + }, + default: 'defineBelow', + description: '', + }, + { + displayName: 'Inputs to Ignore', + name: 'inputsToIgnore', + type: 'string', + displayOptions: { + show: { + resource: [ + 'row', + ], + 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: { + resource: [ + 'row', + ], + operation: [ + 'create', + 'update', + ], + dataToSend: [ + 'defineBelow', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Field', + name: 'fieldValues', + values: [ + { + displayName: 'Field Name', + name: 'fieldId', + type: 'options', + typeOptions: { + loadOptionsDependsOn: [ + 'tableId', + ], + loadOptionsMethod: 'getTableColumns', + }, + default: '', + }, + { + displayName: 'Field Value', + name: 'fieldValue', + type: 'string', + default: '', + }, + ], + }, + ], + }, + /* -------------------------------------------------------------------------- */ + /* row:delete */ + /* -------------------------------------------------------------------------- */ + ...getFilters( + ['row'], + ['delete'], + { + includeNoneOption: false, + filterTypeDisplayName: 'Select Type', + filterStringDisplayName: 'Select Condition (String)', + filterFixedCollectionDisplayName: 'Select Conditions', + mustMatchOptions: [ + { + name: 'Any Select Condition', + value: 'anyFilter', + }, + { + name: 'All Select Conditions', + value: 'allFilters', + }, + ]}), + /* -------------------------------------------------------------------------- */ + /* row:get */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Select Conditions', + name: 'primaryKey', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + displayOptions: { + show: { + resource: [ + 'row', + ], + operation: [ + 'get', + ], + }, + }, + default: '', + placeholder: 'Add Condition', + options: [ + { + displayName: 'Conditions', + name: 'conditions', + values: [ + { + displayName: 'Name', + name: 'keyName', + type: 'options', + typeOptions: { + loadOptionsDependsOn: [ + 'tableId', + ], + loadOptionsMethod: 'getTableColumns', + }, + default: '', + description: '', + }, + { + displayName: 'Value', + name: 'keyValue', + type: 'string', + default: '', + }, + ], + }, + ], + }, + /* -------------------------------------------------------------------------- */ + /* row:getAll */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + noDataExpression: true, + displayOptions: { + show: { + resource: [ + 'row', + ], + operation: [ + 'getAll', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: [ + 'row', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + }, + default: 50, + description: 'How many results to return', + }, + ...getFilters(['row'], ['getAll'], {}), +]; diff --git a/packages/nodes-base/nodes/Supabase/Supabase.node.ts b/packages/nodes-base/nodes/Supabase/Supabase.node.ts new file mode 100644 index 0000000000000..49f41b0f28340 --- /dev/null +++ b/packages/nodes-base/nodes/Supabase/Supabase.node.ts @@ -0,0 +1,351 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + ICredentialDataDecryptedObject, + ICredentialsDecrypted, + ICredentialTestFunctions, + IDataObject, + ILoadOptionsFunctions, + INodeExecutionData, + INodePropertyOptions, + INodeType, + INodeTypeDescription, + NodeCredentialTestResult, + NodeOperationError, +} from 'n8n-workflow'; + +import { + buildGetQuery, + buildOrQuery, + buildQuery, + supabaseApiRequest, + validateCrendentials, +} from './GenericFunctions'; + +import { + rowFields, + rowOperations, +} from './RowDescription'; + +export type FieldsUiValues = Array<{ + fieldId: string; + fieldValue: string; +}>; + +export class Supabase implements INodeType { + description: INodeTypeDescription = { + displayName: 'Supabase', + name: 'supabase', + icon: 'file:supabase.svg', + group: ['input'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Add, get, delete and update data in a table', + defaults: { + name: 'Supabase', + color: '#ea5929', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'supabaseApi', + required: true, + testedBy: 'supabaseApiCredentialTest', + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + noDataExpression: true, + options: [ + { + name: 'Row', + value: 'row', + }, + ], + default: 'row', + }, + ...rowOperations, + ...rowFields, + ], + }; + + methods = { + loadOptions: { + async getTables(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const { paths } = await supabaseApiRequest.call(this, 'GET', '/',); + for (const path of Object.keys(paths)) { + //omit introspection path + if (path === '/') continue; + returnData.push({ + name: path.replace('/', ''), + value: path.replace('/', ''), + }); + } + return returnData; + }, + async getTableColumns(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const tableName = this.getCurrentNodeParameter('tableId') as string; + const { definitions } = await supabaseApiRequest.call(this, 'GET', '/',); + for (const column of Object.keys(definitions[tableName].properties)) { + returnData.push({ + name: `${column} - (${definitions[tableName].properties[column].type})`, + value: column, + }); + } + return returnData; + }, + }, + credentialTest: { + async supabaseApiCredentialTest(this: ICredentialTestFunctions, credential: ICredentialsDecrypted): Promise { + try { + await validateCrendentials.call(this, credential.data as ICredentialDataDecryptedObject); + } catch (error) { + return { + status: 'Error', + message: 'The Service Key is invalid', + }; + } + + return { + status: 'OK', + message: 'Connection successful!', + }; + }, + }, + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: IDataObject[] = []; + const length = (items.length as unknown) as number; + const qs: IDataObject = {}; + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + + if (resource === 'row') { + if (operation === 'create') { + const records: IDataObject[] = []; + const tableId = this.getNodeParameter('tableId', 0) as string; + for (let i = 0; i < length; i++) { + const record: 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 inputDataToIgnore = rawInputsToIgnore.split(',').map(c => c.trim()); + + for (const key of incomingKeys) { + if (inputDataToIgnore.includes(key)) continue; + record[key] = items[i].json[key]; + } + } else { + const fields = this.getNodeParameter('fieldsUi.fieldValues', i, []) as FieldsUiValues; + for (const field of fields) { + record[`${field.fieldId}`] = field.fieldValue; + } + } + records.push(record); + } + const endpoint = `/${tableId}`; + let createdRow; + + try { + createdRow = await supabaseApiRequest.call(this, 'POST', endpoint, records); + returnData.push(...createdRow); + } catch (error) { + if (this.continueOnFail()) { + returnData.push({ error: error.description }); + } else { + throw error; + } + } + } + + if (operation === 'delete') { + const tableId = this.getNodeParameter('tableId', 0) as string; + const filterType = this.getNodeParameter('filterType', 0) as string; + let endpoint = `/${tableId}`; + for (let i = 0; i < length; i++) { + + if (filterType === 'manual') { + const matchType = this.getNodeParameter('matchType', 0) as string; + const keys = this.getNodeParameter('filters.conditions', i, []) as IDataObject[]; + + if (!keys.length) { + throw new NodeOperationError(this.getNode(), 'At least one select condition must be defined'); + } + + if (matchType === 'allFilters') { + const data = keys.reduce((obj, value) => buildQuery(obj, value), {}); + Object.assign(qs, data); + } + if (matchType === 'anyFilter') { + const data = keys.map((key) => buildOrQuery(key)); + Object.assign(qs, { or: `(${data.join(',')})` }); + } + } + + if (filterType === 'string') { + const filterString = this.getNodeParameter('filterString', i) as string; + endpoint = `${endpoint}?${encodeURI(filterString)}`; + } + + let rows; + + try { + rows = await supabaseApiRequest.call(this, 'DELETE', endpoint, {}, qs); + } catch (error) { + if (this.continueOnFail()) { + returnData.push({ error: error.description }); + continue; + } + } + returnData.push(...rows); + } + } + + if (operation === 'get') { + const tableId = this.getNodeParameter('tableId', 0) as string; + const endpoint = `/${tableId}`; + + for (let i = 0; i < length; i++) { + const keys = this.getNodeParameter('filters.conditions', i, []) as IDataObject[]; + const data = keys.reduce((obj, value) => buildGetQuery(obj, value), {}); + Object.assign(qs, data); + let rows; + + if (!keys.length) { + throw new NodeOperationError(this.getNode(), 'At least one select condition must be defined'); + } + + try { + rows = await supabaseApiRequest.call(this, 'GET', endpoint, {}, qs); + } catch (error) { + if (this.continueOnFail()) { + returnData.push({ error: error.description }); + continue; + } + } + returnData.push(...rows); + } + } + + if (operation === 'getAll') { + const tableId = this.getNodeParameter('tableId', 0) as string; + const returnAll = this.getNodeParameter('returnAll', 0) as boolean; + const filterType = this.getNodeParameter('filterType', 0) as string; + let endpoint = `/${tableId}`; + for (let i = 0; i < length; i++) { + + if (filterType === 'manual') { + const matchType = this.getNodeParameter('matchType', 0) as string; + const keys = this.getNodeParameter('filters.conditions', i, []) as IDataObject[]; + + if (keys.length !== 0) { + if (matchType === 'allFilters') { + const data = keys.reduce((obj, value) => buildQuery(obj, value), {}); + Object.assign(qs, data); + } + if (matchType === 'anyFilter') { + const data = keys.map((key) => buildOrQuery(key)); + Object.assign(qs, { or: `(${data.join(',')})` }); + } + } + } + + if (filterType === 'string') { + const filterString = this.getNodeParameter('filterString', i) as string; + endpoint = `${endpoint}?${encodeURI(filterString)}`; + } + + if (returnAll === false) { + qs.limit = this.getNodeParameter('limit', 0) as number; + } + + let rows; + + try { + rows = await supabaseApiRequest.call(this, 'GET', endpoint, {}, qs); + } catch (error) { + if (this.continueOnFail()) { + returnData.push({ error: error.description }); + continue; + } + } + returnData.push(...rows); + } + } + + if (operation === 'update') { + const tableId = this.getNodeParameter('tableId', 0) as string; + const filterType = this.getNodeParameter('filterType', 0) as string; + let endpoint = `/${tableId}`; + for (let i = 0; i < length; i++) { + + if (filterType === 'manual') { + const matchType = this.getNodeParameter('matchType', 0) as string; + const keys = this.getNodeParameter('filters.conditions', i, []) as IDataObject[]; + + if (!keys.length) { + throw new NodeOperationError(this.getNode(), 'At least one select condition must be defined'); + } + + if (matchType === 'allFilters') { + const data = keys.reduce((obj, value) => buildQuery(obj, value), {}); + Object.assign(qs, data); + } + if (matchType === 'anyFilter') { + const data = keys.map((key) => buildOrQuery(key)); + Object.assign(qs, { or: `(${data.join(',')})` }); + } + } + + if (filterType === 'string') { + const filterString = this.getNodeParameter('filterString', i) as string; + endpoint = `${endpoint}?${encodeURI(filterString)}`; + } + + const record: 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 inputDataToIgnore = rawInputsToIgnore.split(',').map(c => c.trim()); + + for (const key of incomingKeys) { + if (inputDataToIgnore.includes(key)) continue; + record[key] = items[i].json[key]; + } + } else { + const fields = this.getNodeParameter('fieldsUi.fieldValues', i, []) as FieldsUiValues; + for (const field of fields) { + record[`${field.fieldId}`] = field.fieldValue; + } + } + let updatedRow; + + try { + updatedRow = await supabaseApiRequest.call(this, 'PATCH', endpoint, record, qs); + returnData.push(...updatedRow); + } catch (error) { + if (this.continueOnFail()) { + returnData.push({ error: error.description }); + continue; + } + } + } + } + } + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/nodes/Supabase/supabase.svg b/packages/nodes-base/nodes/Supabase/supabase.svg new file mode 100644 index 0000000000000..ad802ac16ae9e --- /dev/null +++ b/packages/nodes-base/nodes/Supabase/supabase.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 2e62794fd96e6..e2d4f9df72be4 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -270,6 +270,7 @@ "dist/credentials/StrapiApi.credentials.js", "dist/credentials/StravaOAuth2Api.credentials.js", "dist/credentials/StripeApi.credentials.js", + "dist/credentials/SupabaseApi.credentials.js", "dist/credentials/SurveyMonkeyApi.credentials.js", "dist/credentials/SurveyMonkeyOAuth2Api.credentials.js", "dist/credentials/SyncroMspApi.credentials.js", @@ -604,6 +605,7 @@ "dist/nodes/Strava/StravaTrigger.node.js", "dist/nodes/Stripe/Stripe.node.js", "dist/nodes/Stripe/StripeTrigger.node.js", + "dist/nodes/Supabase/Supabase.node.js", "dist/nodes/SurveyMonkey/SurveyMonkeyTrigger.node.js", "dist/nodes/Switch/Switch.node.js", "dist/nodes/SyncroMSP/SyncroMsp.node.js",