first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled

This commit is contained in:
2026-03-17 16:22:57 +03:30
commit 3d5eaf9445
15349 changed files with 2847338 additions and 0 deletions
@@ -0,0 +1,18 @@
{
"node": "n8n-nodes-base.bubble",
"nodeVersion": "1.0",
"codexVersion": "1.0",
"categories": ["Development"],
"resources": {
"credentialDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/credentials/bubble/"
}
],
"primaryDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.bubble/"
}
]
}
}
@@ -0,0 +1,184 @@
import type {
IExecuteFunctions,
IDataObject,
INodeExecutionData,
INodeType,
INodeTypeDescription,
} from 'n8n-workflow';
import { NodeConnectionTypes, NodeOperationError } from 'n8n-workflow';
import { bubbleApiRequest, bubbleApiRequestAllItems, validateJSON } from './GenericFunctions';
import { objectFields, objectOperations } from './ObjectDescription';
export class Bubble implements INodeType {
description: INodeTypeDescription = {
displayName: 'Bubble',
name: 'bubble',
icon: { light: 'file:bubble.svg', dark: 'file:bubble.dark.svg' },
group: ['transform'],
version: 1,
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Consume the Bubble Data API',
defaults: {
name: 'Bubble',
},
usableAsTool: true,
inputs: [NodeConnectionTypes.Main],
outputs: [NodeConnectionTypes.Main],
credentials: [
{
name: 'bubbleApi',
required: true,
},
],
properties: [
{
displayName: 'Resource',
name: 'resource',
type: 'options',
noDataExpression: true,
options: [
{
name: 'Object',
value: 'object',
},
],
default: 'object',
},
...objectOperations,
...objectFields,
],
};
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 qs: IDataObject = {};
const returnData: INodeExecutionData[] = [];
for (let i = 0; i < items.length; i++) {
if (resource === 'object') {
// *********************************************************************
// object
// *********************************************************************
// https://bubble.io/reference#API
if (operation === 'create') {
// ----------------------------------
// object: create
// ----------------------------------
const typeNameInput = this.getNodeParameter('typeName', i) as string;
const typeName = typeNameInput.replace(/\s/g, '').toLowerCase();
const { property } = this.getNodeParameter('properties', i) as {
property: [{ key: string; value: string }];
};
const body = {} as IDataObject;
property.forEach((data) => (body[data.key] = data.value));
responseData = await bubbleApiRequest.call(this, 'POST', `/obj/${typeName}`, body, {});
} else if (operation === 'delete') {
// ----------------------------------
// object: delete
// ----------------------------------
const typeNameInput = this.getNodeParameter('typeName', i) as string;
const typeName = typeNameInput.replace(/\s/g, '').toLowerCase();
const objectId = this.getNodeParameter('objectId', i) as string;
const endpoint = `/obj/${typeName}/${objectId}`;
responseData = await bubbleApiRequest.call(this, 'DELETE', endpoint, {}, {});
responseData = { success: true };
} else if (operation === 'get') {
// ----------------------------------
// object: get
// ----------------------------------
const typeNameInput = this.getNodeParameter('typeName', i) as string;
const typeName = typeNameInput.replace(/\s/g, '').toLowerCase();
const objectId = this.getNodeParameter('objectId', i) as string;
const endpoint = `/obj/${typeName}/${objectId}`;
responseData = await bubbleApiRequest.call(this, 'GET', endpoint, {}, {});
responseData = responseData.response;
} else if (operation === 'getAll') {
// ----------------------------------
// object: getAll
// ----------------------------------
const returnAll = this.getNodeParameter('returnAll', 0);
const typeNameInput = this.getNodeParameter('typeName', i) as string;
const typeName = typeNameInput.replace(/\s/g, '').toLowerCase();
const endpoint = `/obj/${typeName}`;
const jsonParameters = this.getNodeParameter('jsonParameters', 0);
const options = this.getNodeParameter('options', i);
if (!jsonParameters) {
if (options.filters) {
const { filter } = options.filters as IDataObject;
qs.constraints = JSON.stringify(filter);
}
} else {
const filter = options.filtersJson as string;
const data = validateJSON(filter);
if (data === undefined) {
throw new NodeOperationError(this.getNode(), 'Filters must be a valid JSON', {
itemIndex: i,
});
}
qs.constraints = JSON.stringify(data);
}
if (options.sort) {
const { sortValue } = options.sort as IDataObject;
Object.assign(qs, sortValue);
}
if (returnAll) {
responseData = await bubbleApiRequestAllItems.call(this, 'GET', endpoint, {}, qs);
} else {
qs.limit = this.getNodeParameter('limit', 0);
responseData = await bubbleApiRequest.call(this, 'GET', endpoint, {}, qs);
responseData = responseData.response.results;
}
} else if (operation === 'update') {
// ----------------------------------
// object: update
// ----------------------------------
const typeNameInput = this.getNodeParameter('typeName', i) as string;
const typeName = typeNameInput.replace(/\s/g, '').toLowerCase();
const objectId = this.getNodeParameter('objectId', i) as string;
const endpoint = `/obj/${typeName}/${objectId}`;
const { property } = this.getNodeParameter('properties', i) as {
property: [{ key: string; value: string }];
};
const body = {} as IDataObject;
property.forEach((data) => (body[data.key] = data.value));
responseData = await bubbleApiRequest.call(this, 'PATCH', endpoint, body, {});
responseData = { success: true };
}
}
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(responseData as IDataObject),
{ itemData: { item: i } },
);
returnData.push(...executionData);
}
return [returnData];
}
}
@@ -0,0 +1,92 @@
import type {
IExecuteFunctions,
IHookFunctions,
IDataObject,
ILoadOptionsFunctions,
JsonObject,
IHttpRequestMethods,
IRequestOptions,
} from 'n8n-workflow';
import { NodeApiError } from 'n8n-workflow';
/**
* Make an authenticated API request to Bubble.
*/
export async function bubbleApiRequest(
this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions,
method: IHttpRequestMethods,
endpoint: string,
body: IDataObject,
qs: IDataObject,
) {
const { apiToken, appName, domain, environment, hosting } = await this.getCredentials<{
apiToken: string;
appName: string;
domain: string;
environment: 'development' | 'live';
hosting: 'bubbleHosted' | 'selfHosted';
}>('bubbleApi');
const rootUrl = hosting === 'bubbleHosted' ? `https://${appName}.bubbleapps.io` : domain;
const urlSegment = environment === 'development' ? '/version-test/api/1.1' : '/api/1.1';
const options: IRequestOptions = {
headers: {
'user-agent': 'n8n',
Authorization: `Bearer ${apiToken}`,
},
method,
uri: `${rootUrl}${urlSegment}${endpoint}`,
qs,
body,
json: true,
};
if (!Object.keys(body).length) {
delete options.body;
}
if (!Object.keys(qs).length) {
delete options.qs;
}
try {
return await this.helpers.request(options);
} catch (error) {
throw new NodeApiError(this.getNode(), error as JsonObject);
}
}
/**
* Make an authenticated API request to Bubble and return all results.
*/
export async function bubbleApiRequestAllItems(
this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions,
method: IHttpRequestMethods,
endpoint: string,
body: IDataObject,
qs: IDataObject,
) {
const returnData: IDataObject[] = [];
let responseData;
qs.limit = 100;
qs.cursor = 0;
do {
responseData = await bubbleApiRequest.call(this, method, endpoint, body, qs);
qs.cursor += qs.limit;
returnData.push.apply(returnData, responseData.response.results as IDataObject[]);
} while (responseData.response.remaining !== 0);
return returnData;
}
export function validateJSON(json: string | undefined): any {
let result;
try {
result = JSON.parse(json!);
} catch (exception) {
result = undefined;
}
return result;
}
@@ -0,0 +1,472 @@
import type { INodeProperties } from 'n8n-workflow';
export const objectOperations: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
default: 'get',
options: [
{
name: 'Create',
value: 'create',
action: 'Create an object',
},
{
name: 'Delete',
value: 'delete',
action: 'Delete an object',
},
{
name: 'Get',
value: 'get',
action: 'Get an object',
},
{
name: 'Get Many',
value: 'getAll',
action: 'Get many objects',
},
{
name: 'Update',
value: 'update',
action: 'Update an object',
},
],
displayOptions: {
show: {
resource: ['object'],
},
},
},
];
export const objectFields: INodeProperties[] = [
// ----------------------------------
// object: create
// ----------------------------------
{
displayName: 'Type Name',
name: 'typeName',
type: 'string',
required: true,
description: 'Name of data type of the object to create',
default: '',
displayOptions: {
show: {
resource: ['object'],
operation: ['create'],
},
},
},
{
displayName: 'Properties',
name: 'properties',
placeholder: 'Add Property',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
default: {},
displayOptions: {
show: {
resource: ['object'],
operation: ['create'],
},
},
options: [
{
displayName: 'Property',
name: 'property',
values: [
{
displayName: 'Key',
name: 'key',
type: 'string',
default: '',
description: 'Field to set for the object to create',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
description: 'Value to set for the object to create',
},
],
},
],
},
// ----------------------------------
// object: get
// ----------------------------------
{
displayName: 'Type Name',
name: 'typeName',
type: 'string',
required: true,
description: 'Name of data type of the object to retrieve',
default: '',
displayOptions: {
show: {
resource: ['object'],
operation: ['get', 'delete'],
},
},
},
{
displayName: 'Object ID',
name: 'objectId',
type: 'string',
required: true,
description: 'ID of the object to retrieve',
default: '',
displayOptions: {
show: {
resource: ['object'],
operation: ['get', 'delete'],
},
},
},
// ----------------------------------
// object: update
// ----------------------------------
{
displayName: 'Type Name',
name: 'typeName',
type: 'string',
required: true,
description: 'Name of data type of the object to update',
default: '',
displayOptions: {
show: {
resource: ['object'],
operation: ['update'],
},
},
},
{
displayName: 'Object ID',
name: 'objectId',
type: 'string',
required: true,
description: 'ID of the object to update',
default: '',
displayOptions: {
show: {
resource: ['object'],
operation: ['update'],
},
},
},
{
displayName: 'Properties',
name: 'properties',
placeholder: 'Add Property',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
default: {},
displayOptions: {
show: {
resource: ['object'],
operation: ['update'],
},
},
options: [
{
displayName: 'Property',
name: 'property',
values: [
{
displayName: 'Key',
name: 'key',
type: 'string',
default: '',
description: 'Field to set for the object to create',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
description: 'Value to set for the object to create',
},
],
},
],
},
// ----------------------------------
// object:getAll
// ----------------------------------
{
displayName: 'Type Name',
name: 'typeName',
type: 'string',
required: true,
description: 'Name of data type of the object to create',
default: '',
displayOptions: {
show: {
resource: ['object'],
operation: ['getAll'],
},
},
},
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
resource: ['object'],
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: {
resource: ['object'],
operation: ['getAll'],
returnAll: [false],
},
},
typeOptions: {
minValue: 1,
maxValue: 100,
},
default: 100,
description: 'Max number of results to return',
},
{
displayName: 'JSON Parameters',
name: 'jsonParameters',
type: 'boolean',
default: false,
displayOptions: {
show: {
resource: ['object'],
operation: ['getAll'],
},
},
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
displayOptions: {
show: {
operation: ['getAll'],
resource: ['object'],
},
},
default: {},
placeholder: 'Add Field',
options: [
{
displayName: 'Filters',
name: 'filters',
placeholder: 'Add Filter',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
displayOptions: {
show: {
'/jsonParameters': [false],
},
},
default: {},
options: [
{
displayName: 'Filter',
name: 'filter',
values: [
{
displayName: 'Key',
name: 'key',
type: 'string',
default: '',
description: 'Field to set for the object to create',
},
{
displayName: 'Constrain',
name: 'constraint_type',
type: 'options',
// eslint-disable-next-line n8n-nodes-base/node-param-options-type-unsorted-items
options: [
{
name: 'Equals',
value: 'equals',
description: 'Use to test strict equality, for all field types',
},
{
name: 'Not Equal',
value: 'not equal',
description: 'Use to test strict equality, for all field types',
},
{
name: 'Is Empty',
value: 'is_empty',
description:
"Use to test whether a thing's given field is empty, for all field types",
},
{
name: 'Is Not Empty',
value: 'is_not_empty',
description:
"Use to test whether a thing's given field is not empty, for all field types",
},
{
name: 'Text Contains',
value: 'text contains',
description:
'Use to test if a text field contains a string, for text fields only',
},
{
name: 'Not Text Contains',
value: 'not text contains',
description:
'Use to test if a text field does not contain a string, for text fields only',
},
{
name: 'Greater Than',
value: 'greater than',
description:
"Use to compare a thing's field value relative to a string or number, for text, number, and date fields",
},
{
name: 'Less Than',
value: 'less than',
description:
"Use to compare a thing's field value relative to a string or number, for text, number, and date fields",
},
{
name: 'In',
value: 'in',
description:
"Use to test whether a thing's field is in a list, for all field types",
},
{
name: 'Not In',
value: 'not in',
description:
"Use to test whether a thing's field is not in a list, for all field types",
},
{
name: 'Contains',
value: 'contains',
description:
'Use to test whether a list field contains an entry, for list fields only',
},
{
name: 'Not Contains',
value: 'not contains',
description:
'Use to test whether a list field does not contains an entry, for list fields only',
},
{
name: 'Empty',
value: 'empty',
description: 'Use to test whether a list field is empty, for list fields only',
},
{
name: 'Not Empty',
value: 'not empty',
description:
'Use to test whether a list field is not empty, for list fields only',
},
{
name: 'Geographic Search',
value: 'geographic_search',
description:
'Use to test if the current thing is within a radius from a central address. To use this, the value sent with the constraint must have an address and a range. See <a href="https://manual.bubble.io/core-resources/api/data-api">link</a>.',
},
],
default: '',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
displayOptions: {
hide: {
constraint_type: ['is_empty', 'is_not_empty', 'empty', 'not empty'],
},
},
default: '',
description: 'Value to set for the object to create',
},
],
},
],
},
{
displayName: 'Filters (JSON)',
name: 'filtersJson',
type: 'json',
default: '',
displayOptions: {
show: {
'/jsonParameters': [true],
},
},
placeholder:
'[ { "key": "name", "constraint_type": "text contains", "value": "cafe" } , { "key": "address", "constraint_type": "geographic_search", "value": { "range":10, "origin_address":"New York" } } ]',
description:
'Refine the list that is returned by the Data API with search constraints, exactly as you define a search in Bubble. See <a href="https://manual.bubble.io/core-resources/api/data-api#search-constraints">link</a>.',
},
{
displayName: 'Sort',
name: 'sort',
placeholder: 'Add Sort',
type: 'fixedCollection',
typeOptions: {
multipleValues: false,
},
default: {},
options: [
{
displayName: 'Sort',
name: 'sortValue',
values: [
{
displayName: 'Sort Field',
name: 'sort_field',
type: 'string',
default: '',
description:
'Specify the field to use for sorting. Either use a fielddefined for the current typeor use <code>_random_sorting</code> to get the entries in a random order.',
},
{
displayName: 'Descending',
name: 'descending',
type: 'boolean',
default: false,
},
{
displayName: 'Geo Reference',
name: 'geo_reference',
type: 'string',
default: '',
description:
"When the field's type is geographic address, you need to add another parameter geo_reference and provide an address as a string",
},
],
},
],
},
],
},
];
@@ -0,0 +1,12 @@
{
"type": "object",
"properties": {
"id": {
"type": "string"
},
"status": {
"type": "string"
}
},
"version": 1
}
@@ -0,0 +1,9 @@
{
"type": "object",
"properties": {
"success": {
"type": "boolean"
}
},
"version": 1
}
@@ -0,0 +1,21 @@
{
"type": "object",
"properties": {
"_id": {
"type": "string"
},
"agencia": {
"type": "string"
},
"Created By": {
"type": "string"
},
"Created Date": {
"type": "string"
},
"Modified Date": {
"type": "string"
}
},
"version": 2
}
@@ -0,0 +1,18 @@
{
"type": "object",
"properties": {
"_id": {
"type": "string"
},
"Created By": {
"type": "string"
},
"Created Date": {
"type": "string"
},
"Modified Date": {
"type": "string"
}
},
"version": 1
}
@@ -0,0 +1,9 @@
{
"type": "object",
"properties": {
"success": {
"type": "boolean"
}
},
"version": 1
}
@@ -0,0 +1,4 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M23.7491 10.3853C20.1543 10.3853 16.6111 11.9286 13.9061 14.9628V0H8.55322V25.1909C8.55322 25.1915 8.55322 25.1921 8.55322 25.1928C8.55322 33.3706 15.1826 40 23.3606 40C31.5384 40 38.1679 33.3706 38.1679 25.1928C38.1679 17.0149 31.9269 10.3853 23.7491 10.3853ZM23.3606 34.2968C18.3324 34.2968 14.2563 30.2207 14.2563 25.1927C14.2563 20.1646 18.3324 16.0884 23.3606 16.0884C28.3886 16.0884 32.4647 20.1646 32.4647 25.1927C32.4647 30.2208 28.3886 34.2968 23.3606 34.2968Z" fill="white"/>
<path d="M4.66161 32.6767C2.6393 32.6767 1 34.3161 1 36.3383C1 38.3605 2.6393 39.9999 4.66161 39.9999C6.68391 39.9999 8.32322 38.3605 8.32322 36.3383C8.32322 34.3161 6.68391 32.6767 4.66161 32.6767Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 813 B

@@ -0,0 +1,4 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M23.7491 10.3853C20.1543 10.3853 16.6111 11.9286 13.9061 14.9628V0H8.55322V25.1909C8.55322 25.1915 8.55322 25.1921 8.55322 25.1928C8.55322 33.3706 15.1826 40 23.3606 40C31.5384 40 38.1679 33.3706 38.1679 25.1928C38.1679 17.0149 31.9269 10.3853 23.7491 10.3853ZM23.3606 34.2968C18.3324 34.2968 14.2563 30.2207 14.2563 25.1927C14.2563 20.1646 18.3324 16.0884 23.3606 16.0884C28.3886 16.0884 32.4647 20.1646 32.4647 25.1927C32.4647 30.2208 28.3886 34.2968 23.3606 34.2968Z" fill="#262626"/>
<path d="M4.66161 32.6767C2.6393 32.6767 1 34.3161 1 36.3383C1 38.3605 2.6393 39.9999 4.66161 39.9999C6.68391 39.9999 8.32322 38.3605 8.32322 36.3383C8.32322 34.3161 6.68391 32.6767 4.66161 32.6767Z" fill="#0000FF"/>
</svg>

After

Width:  |  Height:  |  Size: 817 B