Add Activepieces integration for workflow automation

- Add Activepieces fork with SmoothSchedule custom piece
- Create integrations app with Activepieces service layer
- Add embed token endpoint for iframe integration
- Create Automations page with embedded workflow builder
- Add sidebar visibility fix for embed mode
- Add list inactive customers endpoint to Public API
- Include SmoothSchedule triggers: event created/updated/cancelled
- Include SmoothSchedule actions: create/update/cancel events, list resources/services/customers

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
poduck
2025-12-18 22:59:37 -05:00
parent 9848268d34
commit 3aa7199503
16292 changed files with 1284892 additions and 4708 deletions

View File

@@ -0,0 +1,55 @@
import { createAction, Property } from "@activepieces/pieces-framework";
import { supabaseAuth } from "../../index";
import { createClient } from "@supabase/supabase-js";
import { supabaseCommon } from "../common/props";
export const createRow = createAction({
name: 'create_row',
displayName: 'Create Row',
description: 'Create a new row in a table',
auth: supabaseAuth,
props: {
table_name: supabaseCommon.table_name,
row_data: supabaseCommon.table_columns,
return_row: Property.Checkbox({
displayName: 'Return Created Row',
description: 'Whether to return the created row',
required: false,
defaultValue: true,
}),
},
async run(context) {
const { table_name, row_data, return_row } = context.propsValue;
const { url, apiKey } = context.auth.props;
const supabase = createClient(url, apiKey);
const baseQuery = supabase.from(table_name as string).insert(row_data);
const { data, error } = return_row
? await baseQuery.select()
: await baseQuery;
if (error) {
let errorMessage = error.message || 'Unknown error occurred';
if (error.code === '23505') {
errorMessage = `Duplicate value: ${error.message}`;
} else if (error.code === '23503') {
errorMessage = `Foreign key constraint violation: ${error.message}`;
} else if (error.code === '23502') {
errorMessage = `Required field missing: ${error.message}`;
} else if (error.code === '42703') {
errorMessage = `Column does not exist: ${error.message}`;
} else if (error.code === '42P01') {
errorMessage = `Table does not exist: ${error.message}`;
}
throw new Error(errorMessage);
}
return data;
},
});

View File

@@ -0,0 +1,227 @@
import { createAction, Property } from "@activepieces/pieces-framework";
import { supabaseAuth } from "../../index";
import { createClient } from "@supabase/supabase-js";
import { supabaseCommon } from "../common/props";
export const deleteRows = createAction({
name: 'delete_rows',
displayName: 'Delete Rows',
description: 'Remove rows matching filter criteria from a table',
auth: supabaseAuth,
props: {
table_name: supabaseCommon.table_name,
filter_type: Property.StaticDropdown({
displayName: 'Filter Type',
description: 'How to filter rows for deletion',
required: true,
defaultValue: 'in',
options: {
options: [
{ label: 'Column equals value', value: 'eq' },
{ label: 'Column not equals value', value: 'neq' },
{ label: 'Column is in list', value: 'in' },
{ label: 'Column is greater than', value: 'gt' },
{ label: 'Column is greater than or equal', value: 'gte' },
{ label: 'Column is less than', value: 'lt' },
{ label: 'Column is less than or equal', value: 'lte' },
{ label: 'Column is null', value: 'is_null' },
{ label: 'Column is not null', value: 'is_not_null' },
{ label: 'Column matches pattern (LIKE)', value: 'like' },
{ label: 'Column matches pattern (case-insensitive)', value: 'ilike' }
]
}
}),
filter_column: Property.Dropdown({
auth: supabaseAuth,
displayName: 'Filter Column',
description: 'Select the column to filter on',
required: true,
refreshers: ['table_name'],
options: async ({ auth, table_name }) => {
if (!auth || !table_name) {
return {
disabled: true,
options: [],
placeholder: 'Please select a table first'
};
}
try {
const { url, apiKey } = auth.props;
const supabase = createClient(url, apiKey);
try {
const { data: columns, error } = await supabase.rpc('get_table_columns', {
p_table_name: table_name as unknown as string
});
if (!error && columns && columns.length > 0) {
return {
disabled: false,
options: columns.map((col: any) => ({
label: `${col.column_name} (${col.data_type})`,
value: col.column_name
}))
};
}
} catch (rpcError) {
// Continue to OpenAPI fallback
}
const response = await fetch(`${url}/rest/v1/`, {
method: 'GET',
headers: {
'apikey': apiKey,
'Authorization': `Bearer ${apiKey}`,
'Accept': 'application/openapi+json'
}
});
if (response.ok) {
const openApiSpec = await response.json();
const definitions = openApiSpec.definitions || openApiSpec.components?.schemas || {};
const tableDefinition = definitions[table_name as unknown as string];
if (tableDefinition && tableDefinition.properties) {
const options = Object.entries(tableDefinition.properties).map(([columnName, columnDef]: [string, any]) => {
const type = columnDef.type || 'unknown';
return {
label: `${columnName} (${type})`,
value: columnName
};
});
return {
disabled: false,
options
};
}
}
return {
disabled: true,
options: [],
placeholder: 'Could not load columns'
};
} catch (error) {
return {
disabled: true,
options: [],
placeholder: 'Error loading columns'
};
}
}
}),
filter_value: Property.ShortText({
displayName: 'Filter Value',
description: 'The value to match against (not used for null checks)',
required: false,
}),
filter_values: Property.Array({
displayName: 'Filter Values',
description: 'List of values for "in" filter type',
required: false,
}),
count_deleted: Property.Checkbox({
displayName: 'Count Deleted Rows',
description: 'Whether to count the number of deleted rows',
required: false,
defaultValue: false,
}),
return_deleted: Property.Checkbox({
displayName: 'Return Deleted Rows',
description: 'Whether to return the deleted rows data',
required: false,
defaultValue: false,
})
},
async run(context) {
const {
table_name,
filter_type,
filter_column,
filter_value,
filter_values,
count_deleted,
return_deleted
} = context.propsValue;
const { url, apiKey } = context.auth.props;
const supabase = createClient(url, apiKey);
let deleteQuery = supabase
.from(table_name as string)
.delete({
count: count_deleted ? 'exact' : undefined
});
const columnName = filter_column as string;
switch (filter_type) {
case 'eq':
if (!filter_value) throw new Error('Filter value is required for equality check');
deleteQuery = deleteQuery.eq(columnName, filter_value);
break;
case 'neq':
if (!filter_value) throw new Error('Filter value is required for not-equals check');
deleteQuery = deleteQuery.neq(columnName, filter_value);
break;
case 'in':
if (!filter_values || filter_values.length === 0) {
throw new Error('Filter values are required for "in" filter type');
}
deleteQuery = deleteQuery.in(columnName, filter_values);
break;
case 'gt':
if (!filter_value) throw new Error('Filter value is required for greater-than check');
deleteQuery = deleteQuery.gt(columnName, filter_value);
break;
case 'gte':
if (!filter_value) throw new Error('Filter value is required for greater-than-or-equal check');
deleteQuery = deleteQuery.gte(columnName, filter_value);
break;
case 'lt':
if (!filter_value) throw new Error('Filter value is required for less-than check');
deleteQuery = deleteQuery.lt(columnName, filter_value);
break;
case 'lte':
if (!filter_value) throw new Error('Filter value is required for less-than-or-equal check');
deleteQuery = deleteQuery.lte(columnName, filter_value);
break;
case 'is_null':
deleteQuery = deleteQuery.is(columnName, null);
break;
case 'is_not_null':
deleteQuery = deleteQuery.not(columnName, 'is', null);
break;
case 'like':
if (!filter_value) throw new Error('Filter value is required for like pattern matching');
deleteQuery = deleteQuery.like(columnName, filter_value);
break;
case 'ilike':
if (!filter_value) throw new Error('Filter value is required for case-insensitive like pattern matching');
deleteQuery = deleteQuery.ilike(columnName, filter_value);
break;
default:
throw new Error(`Unsupported filter type: ${filter_type}`);
}
const { data, error, count } = return_deleted
? await deleteQuery.select()
: await deleteQuery;
if (error) {
throw error;
}
const result: any = {
success: true,
deleted_rows: return_deleted ? data : undefined,
};
if (count_deleted) {
result.deleted_count = count;
}
return result;
}
});

View File

@@ -0,0 +1,197 @@
import { createAction, Property } from "@activepieces/pieces-framework";
import { supabaseAuth } from "../../index";
import { createClient } from "@supabase/supabase-js";
import { supabaseCommon } from "../common/props";
type FilterOperator = 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte' | 'like' | 'ilike' | 'is' | 'in' | 'contains' | 'containedBy';
interface Filter {
field: string;
operator: FilterOperator;
value: string | number | boolean | null;
}
export const searchRows = createAction({
name: 'search_rows',
displayName: 'Search Rows',
description: 'Search for rows in a table with filters and pagination',
auth: supabaseAuth,
props: {
table_name: supabaseCommon.table_name,
columns: Property.ShortText({
displayName: 'Columns',
description: 'Columns to return (comma-separated). Leave empty to return all columns.',
required: false,
}),
filters: Property.Array({
displayName: 'Filters',
description: 'List of filters to apply',
required: false,
properties: {
field: Property.ShortText({
displayName: 'Field',
description: 'Field name to filter on (use -> for JSON fields, e.g. address->postcode)',
required: true,
}),
operator: Property.StaticDropdown({
displayName: 'Operator',
description: 'Comparison operator',
required: true,
options: {
options: [
{ label: 'Equals', value: 'eq' },
{ label: 'Not Equals', value: 'neq' },
{ label: 'Greater Than', value: 'gt' },
{ label: 'Greater Than or Equal', value: 'gte' },
{ label: 'Less Than', value: 'lt' },
{ label: 'Less Than or Equal', value: 'lte' },
{ label: 'Like', value: 'like' },
{ label: 'ILike (Case Insensitive)', value: 'ilike' },
{ label: 'Is', value: 'is' },
{ label: 'In', value: 'in' },
{ label: 'Contains', value: 'contains' },
{ label: 'Contained By', value: 'containedBy' },
]
}
}),
value: Property.ShortText({
displayName: 'Value',
description: 'Value to compare against',
required: true,
}),
}
}),
page: Property.Number({
displayName: 'Page',
description: 'Page number for pagination (starts from 1)',
required: false,
defaultValue: 1,
}),
pageSize: Property.Number({
displayName: 'Page Size',
description: 'Number of records per page (max 1000)',
required: false,
defaultValue: 20,
}),
countOption: Property.StaticDropdown({
displayName: 'Count Algorithm',
description: 'Algorithm to use for counting rows',
required: false,
options: {
options: [
{ label: 'Exact', value: 'exact' },
{ label: 'Planned', value: 'planned' },
{ label: 'Estimated', value: 'estimated' },
]
}
}),
},
async run(context) {
const { table_name, columns, filters, page, pageSize, countOption } = context.propsValue;
const { url, apiKey } = context.auth.props;
const currentPage = Math.max(1, page || 1);
const currentPageSize = Math.min(1000, Math.max(1, pageSize || 20));
if (columns && !/^[a-zA-Z0-9_,.\s\->"*]+$/.test(columns)) {
throw new Error('Invalid column specification. Only alphanumeric characters, underscores, commas, dots, arrows, quotes, and asterisks are allowed.');
}
const supabase = createClient(url, apiKey);
let query = supabase.from(table_name as string).select(
columns || '*',
{ count: countOption as 'exact' | 'planned' | 'estimated' | undefined }
);
if (filters && Array.isArray(filters) && filters.length > 0) {
for (const filter of filters as Filter[]) {
if (!filter.field || !filter.operator) {
throw new Error('Filter must have both field and operator specified');
}
if (!/^[a-zA-Z0-9_.\->"]+$/.test(filter.field)) {
throw new Error(`Invalid field name: ${filter.field}. Only alphanumeric characters, underscores, dots, and arrows are allowed.`);
}
try {
switch (filter.operator) {
case 'eq':
query = query.eq(filter.field, filter.value);
break;
case 'neq':
query = query.neq(filter.field, filter.value);
break;
case 'gt':
query = query.gt(filter.field, filter.value);
break;
case 'gte':
query = query.gte(filter.field, filter.value);
break;
case 'lt':
query = query.lt(filter.field, filter.value);
break;
case 'lte':
query = query.lte(filter.field, filter.value);
break;
case 'like':
query = query.like(filter.field, String(filter.value));
break;
case 'ilike':
query = query.ilike(filter.field, String(filter.value));
break;
case 'is':
query = query.is(filter.field, filter.value);
break;
case 'in': {
const inValues = Array.isArray(filter.value) ? filter.value : String(filter.value).split(',');
query = query.in(filter.field, inValues);
break;
}
case 'contains':
if (typeof filter.value === 'string' || Array.isArray(filter.value) || (filter.value && typeof filter.value === 'object')) {
query = query.contains(filter.field, filter.value);
} else {
throw new Error('Contains operator requires string, array, or object value');
}
break;
case 'containedBy':
if (typeof filter.value === 'string' || Array.isArray(filter.value) || (filter.value && typeof filter.value === 'object')) {
query = query.containedBy(filter.field, filter.value);
} else {
throw new Error('ContainedBy operator requires string, array, or object value');
}
break;
default:
throw new Error(`Unsupported filter operator: ${filter.operator}`);
}
} catch (filterError) {
throw new Error(`Failed to apply filter on field '${filter.field}' with operator '${filter.operator}': ${filterError instanceof Error ? filterError.message : 'Unknown error'}`);
}
}
}
const from = (currentPage - 1) * currentPageSize;
const to = from + currentPageSize - 1;
query = query.range(from, to);
const { data, error, count } = await query;
if (error) {
throw new Error(`Database query failed: ${error.message}`);
}
return {
data: data || [],
count: count || 0,
page: currentPage,
pageSize: currentPageSize,
total_pages: count ? Math.ceil(count / currentPageSize) : 0,
range: {
from,
to,
returned: data?.length || 0
}
};
},
});

View File

@@ -0,0 +1,203 @@
import { createAction, Property, DynamicPropsValue } from "@activepieces/pieces-framework";
import { supabaseAuth } from "../../index";
import { createClient } from "@supabase/supabase-js";
import { supabaseCommon } from "../common/props";
export const updateRow = createAction({
name: 'update_row',
displayName: 'Update Row',
description: 'Update rows in a table based on filter criteria',
auth: supabaseAuth,
props: {
table_name: supabaseCommon.table_name,
filter_type: Property.StaticDropdown({
displayName: 'Filter Type',
description: 'How to identify rows to update',
required: true,
defaultValue: 'eq',
options: {
options: [
{ label: 'Column equals value', value: 'eq' },
{ label: 'Column is in list of values', value: 'in' },
{ label: 'Column is greater than value', value: 'gt' }
]
}
}),
filter_column: Property.Dropdown({
auth: supabaseAuth,
displayName: 'Filter Column',
description: 'Select the column to filter on',
required: true,
refreshers: ['table_name'],
options: async ({ auth, table_name }) => {
if (!auth || !table_name) {
return {
disabled: true,
options: [],
placeholder: 'Please select a table first'
};
}
try {
const { url, apiKey } = auth.props;
const supabase = createClient(url, apiKey);
try {
const { data: columns, error } = await supabase.rpc('get_table_columns', {
p_table_name: table_name as unknown as string
});
if (!error && columns && columns.length > 0) {
return {
disabled: false,
options: columns.map((col: any) => ({
label: `${col.column_name} (${col.data_type})`,
value: col.column_name
}))
};
}
} catch (rpcError) {
// Continue to OpenAPI fallback
}
const response = await fetch(`${url}/rest/v1/`, {
method: 'GET',
headers: {
'apikey': apiKey,
'Authorization': `Bearer ${apiKey}`,
'Accept': 'application/openapi+json'
}
});
if (response.ok) {
const openApiSpec = await response.json();
const definitions = openApiSpec.definitions || openApiSpec.components?.schemas || {};
const tableDefinition = definitions[table_name as unknown as string];
if (tableDefinition && tableDefinition.properties) {
const options = Object.entries(tableDefinition.properties).map(([columnName, columnDef]: [string, any]) => {
const type = columnDef.type || 'unknown';
return {
label: `${columnName} (${type})`,
value: columnName
};
});
return {
disabled: false,
options
};
}
}
return {
disabled: true,
options: [],
placeholder: 'Could not load columns'
};
} catch (error) {
return {
disabled: true,
options: [],
placeholder: 'Error loading columns'
};
}
}
}),
filter_value: Property.ShortText({
displayName: 'Filter Value',
description: 'The value to match against (not used for "in list" filter)',
required: false,
}),
filter_values: Property.Array({
displayName: 'Filter Values',
description: 'List of values for "in list" filter type',
required: false,
}),
update_data: supabaseCommon.update_fields,
count_updated: Property.Checkbox({
displayName: 'Count Updated Rows',
description: 'Whether to count the number of updated rows',
required: false,
defaultValue: false,
}),
return_updated: Property.Checkbox({
displayName: 'Return Updated Rows',
description: 'Whether to return the updated rows data',
required: false,
defaultValue: false,
})
},
async run(context) {
const {
table_name,
filter_type,
filter_column,
filter_value,
filter_values,
update_data,
count_updated,
return_updated
} = context.propsValue;
const { url, apiKey } = context.auth.props;
const supabase = createClient(url, apiKey);
let updateQuery = supabase
.from(table_name as string)
.update(update_data, {
count: count_updated ? 'exact' : undefined
});
const columnName = filter_column as string;
switch (filter_type) {
case 'eq':
if (!filter_value) throw new Error('Filter value is required for equality check');
updateQuery = updateQuery.eq(columnName, filter_value);
break;
case 'in':
if (!filter_values || filter_values.length === 0) {
throw new Error('Filter values are required for "in list" filter type');
}
updateQuery = updateQuery.in(columnName, filter_values);
break;
case 'gt':
if (!filter_value) throw new Error('Filter value is required for greater-than check');
updateQuery = updateQuery.gt(columnName, filter_value);
break;
default:
throw new Error(`Unsupported filter type: ${filter_type}`);
}
const { data, error, count } = return_updated
? await updateQuery.select()
: await updateQuery;
if (error) {
let errorMessage = error.message || 'Unknown error occurred';
if (error.code === '23505') {
errorMessage = `Duplicate value: ${error.message}`;
} else if (error.code === '23503') {
errorMessage = `Foreign key constraint violation: ${error.message}`;
} else if (error.code === '42703') {
errorMessage = `Column does not exist: ${error.message}`;
} else if (error.code === '42P01') {
errorMessage = `Table does not exist: ${error.message}`;
}
throw new Error(errorMessage);
}
const result: any = {
success: true,
updated_rows: return_updated ? data : undefined,
};
if (count_updated) {
result.updated_count = count;
}
return result;
}
});

View File

@@ -0,0 +1,44 @@
import { supabaseAuth } from '../../index';
import { Property, createAction } from '@activepieces/pieces-framework';
import { createClient } from '@supabase/supabase-js';
export const uploadFile = createAction({
auth: supabaseAuth,
name: 'upload-file',
displayName: 'Upload File',
description: 'Upload a file to Supabase Storage',
props: {
filePath: Property.ShortText({
displayName: 'File path',
required: true,
}),
bucket: Property.ShortText({
displayName: 'Bucket',
required: true,
}),
file: Property.File({
displayName: 'Base64 or URL',
required: true,
}),
},
async run(context) {
const { url, apiKey } = context.auth.props;
const { file, filePath, bucket } = context.propsValue;
const base64 = file.base64;
// Convert base64 to array buffer
const arrayBuffer = Uint8Array.from(atob(base64), (c) => c.charCodeAt(0));
const supabase = createClient(url, apiKey);
const { data, error } = await supabase.storage
.from(bucket)
.upload(filePath, arrayBuffer);
if (error) {
throw new Error(error.message);
}
const { data: pbData } = supabase.storage
.from(bucket)
.getPublicUrl(filePath);
return {
publicUrl: pbData.publicUrl,
};
},
});

View File

@@ -0,0 +1,182 @@
import { createAction, Property } from "@activepieces/pieces-framework";
import { supabaseAuth } from "../../index";
import { createClient } from "@supabase/supabase-js";
import { supabaseCommon } from "../common/props";
export const upsertRow = createAction({
name: 'upsert_row',
displayName: 'Upsert Row',
description: 'Insert or update a row in a table',
auth: supabaseAuth,
props: {
table_name: supabaseCommon.table_name,
on_conflict: Property.Dropdown({
auth: supabaseAuth,
displayName: 'Conflict Column',
description: 'Select the unique column to determine duplicates (required for upsert to work)',
required: true,
refreshers: ['table_name'],
options: async ({ auth, table_name }) => {
if (!auth || !table_name) {
return {
disabled: true,
options: [],
placeholder: 'Please select a table first'
};
}
try {
const { url, apiKey } = auth.props;
const supabase = createClient(url, apiKey);
try {
const { data: columns, error } = await supabase.rpc('get_table_columns', {
p_table_name: table_name as unknown as string
});
if (!error && columns && columns.length > 0) {
const options = columns.map((col: any) => ({
label: `${col.column_name} (${col.data_type})`,
value: col.column_name
}));
options.sort((a: any, b: any) => {
if (a.value === 'id') return -1;
if (b.value === 'id') return 1;
if (a.value.includes('_id')) return -1;
if (b.value.includes('_id')) return 1;
if (a.value === 'email') return -1;
if (b.value === 'email') return 1;
return 0;
});
return {
disabled: false,
options
};
}
} catch (rpcError) {
// Continue to OpenAPI fallback
}
const response = await fetch(`${url}/rest/v1/`, {
method: 'GET',
headers: {
'apikey': apiKey,
'Authorization': `Bearer ${apiKey}`,
'Accept': 'application/openapi+json'
}
});
if (response.ok) {
const openApiSpec = await response.json();
const definitions = openApiSpec.definitions || openApiSpec.components?.schemas || {};
const tableDefinition = definitions[table_name as unknown as string];
if (tableDefinition && tableDefinition.properties) {
const options = Object.entries(tableDefinition.properties).map(([columnName, columnDef]: [string, any]) => {
const type = columnDef.type || 'unknown';
return {
label: `${columnName} (${type})`,
value: columnName
};
});
options.sort((a: any, b: any) => {
if (a.value === 'id') return -1;
if (b.value === 'id') return 1;
if (a.value.includes('_id')) return -1;
if (b.value.includes('_id')) return 1;
if (a.value === 'email') return -1;
if (b.value === 'email') return 1;
return 0;
});
return {
disabled: false,
options
};
}
}
return {
disabled: true,
options: [],
placeholder: 'Could not load columns'
};
} catch (error) {
return {
disabled: true,
options: [],
placeholder: 'Error loading columns'
};
}
}
}),
row_data: supabaseCommon.upsert_fields,
count_upserted: Property.Checkbox({
displayName: 'Count Upserted Rows',
description: 'Whether to count the number of upserted rows',
required: false,
defaultValue: false,
}),
return_upserted: Property.Checkbox({
displayName: 'Return Upserted Rows',
description: 'Whether to return the upserted rows data',
required: false,
defaultValue: false,
})
},
async run(context) {
const {
table_name,
row_data,
on_conflict,
count_upserted,
return_upserted
} = context.propsValue;
const { url, apiKey } = context.auth.props;
const supabase = createClient(url, apiKey);
const upsertOptions: any = {
onConflict: on_conflict,
count: count_upserted ? 'exact' : undefined
};
const upsertQuery = supabase
.from(table_name as string)
.upsert(row_data, upsertOptions);
const { data, error, count } = return_upserted
? await upsertQuery.select()
: await upsertQuery;
if (error) {
let errorMessage = error.message || 'Unknown error occurred';
if (error.code === '23505') {
errorMessage = `Duplicate value: ${error.message}`;
} else if (error.code === '23503') {
errorMessage = `Foreign key constraint violation: ${error.message}`;
} else if (error.code === '42703') {
errorMessage = `Column does not exist: ${error.message}`;
} else if (error.code === '42P01') {
errorMessage = `Table does not exist: ${error.message}`;
}
throw new Error(errorMessage);
}
const result: any = {
success: true,
upserted_rows: return_upserted ? data : undefined,
};
if (count_upserted) {
result.upserted_count = count;
}
return result;
}
});

View File

@@ -0,0 +1,657 @@
import { Property, DynamicPropsValue } from "@activepieces/pieces-framework";
import { createClient } from "@supabase/supabase-js";
import { supabaseAuth } from "../..";
async function getColumnOptions(auth: any, table_name: string) {
try {
const { url, apiKey } =auth.props
const supabase = createClient(url, apiKey);
try {
const { data: columns, error } = await supabase.rpc('get_table_columns', {
p_table_name: table_name
});
if (!error && columns && columns.length > 0) {
return columns.map((col: any) => ({
label: `${col.column_name} (${col.data_type})`,
value: col.column_name
}));
}
} catch (rpcError) {
// Continue to OpenAPI fallback
}
const response = await fetch(`${url}/rest/v1/`, {
method: 'GET',
headers: {
'apikey': apiKey,
'Authorization': `Bearer ${apiKey}`,
'Accept': 'application/openapi+json'
}
});
if (response.ok) {
const openApiSpec = await response.json();
const definitions = openApiSpec.definitions || openApiSpec.components?.schemas || {};
const tableDefinition = definitions[table_name];
if (tableDefinition && tableDefinition.properties) {
return Object.entries(tableDefinition.properties).map(([columnName, columnDef]: [string, any]) => {
const type = columnDef.type || 'unknown';
return {
label: `${columnName} (${type})`,
value: columnName
};
});
}
}
return [];
} catch (error) {
return [];
}
}
export const supabaseCommon = {
table_name: Property.Dropdown({
auth: supabaseAuth,
displayName: 'Table Name',
description: 'Select a table from your database',
required: true,
refreshers: [],
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
options: [],
placeholder: 'Please connect your Supabase account first.'
};
}
try {
const { url, apiKey } = auth.props;
const supabase = createClient(url, apiKey);
try {
const { data: tables, error } = await supabase.rpc('get_public_tables');
if (!error && tables) {
const tableOptions = tables.map((table: any) => ({
label: table.table_name || table.name || table,
value: table.table_name || table.name || table
}));
return {
disabled: false,
options: tableOptions
};
} else if (error) {
console.log('RPC get_public_tables error:', error);
}
} catch (rpcError) {
console.log('RPC function not available, using OpenAPI spec');
}
let openApiSpec: any;
try {
const response = await fetch(`${url}/rest/v1/`, {
method: 'GET',
headers: {
'apikey': apiKey,
'Authorization': `Bearer ${apiKey}`,
'Accept': 'application/openapi+json'
}
});
if (!response.ok) {
return {
disabled: true,
options: [],
placeholder: 'Error loading tables. Please check your connection and permissions.'
};
}
openApiSpec = await response.json();
} catch (fetchError) {
return {
disabled: true,
options: [],
placeholder: 'Network error. Please check your connection.'
};
}
const paths = openApiSpec.paths || {};
const tableNames = Object.keys(paths)
.filter(path => path.startsWith('/') && !path.includes('{') && path !== '/rpc')
.map(path => path.substring(1))
.filter(name => name && !name.includes('/'))
.sort();
if (tableNames.length === 0) {
return {
disabled: true,
options: [],
placeholder: 'No tables found in your database.'
};
}
const tableOptions = tableNames.map(name => ({
label: name,
value: name
}));
return {
disabled: false,
options: tableOptions
};
} catch (error) {
return {
disabled: true,
options: [],
placeholder: 'Error loading tables. Please check your connection.'
};
}
}
}),
table_columns: Property.DynamicProperties({
auth: supabaseAuth,
displayName: 'Row Data',
description: 'Enter the data for each column',
required: true,
refreshers: ['table_name'],
props: async (propsValue) => {
const { auth, table_name } = propsValue;
const properties: DynamicPropsValue = {};
if (!auth || !table_name) {
return properties;
}
try {
const { url, apiKey } =auth.props
const supabase = createClient(url, apiKey);
let columns: any[] = [];
try {
const { data: rpcColumns, error } = await supabase.rpc('get_table_columns', {
p_table_name: table_name as unknown as string
});
if (!error && rpcColumns && rpcColumns.length > 0) {
columns = rpcColumns;
} else if (error) {
console.log('RPC get_table_columns error:', error);
}
} catch (rpcError) {
console.log('RPC function not available for columns');
}
if (columns.length === 0) {
let openApiSpec: any;
try {
const response = await fetch(`${url}/rest/v1/`, {
method: 'GET',
headers: {
'apikey': apiKey,
'Authorization': `Bearer ${apiKey}`,
'Accept': 'application/openapi+json'
}
});
if (!response.ok) {
properties['error'] = Property.MarkDown({
value: `Error loading columns for table "${table_name}". Please check your connection and permissions.`
});
return properties;
}
openApiSpec = await response.json();
} catch (fetchError) {
properties['error'] = Property.MarkDown({
value: `Network error loading columns for table "${table_name}". Please check your connection.`
});
return properties;
}
const definitions = openApiSpec.definitions || openApiSpec.components?.schemas || {};
const tableDefinition = definitions[table_name as unknown as string];
if (!tableDefinition || !tableDefinition.properties) {
properties['info'] = Property.MarkDown({
value: `No columns found for table "${table_name}". Please check if the table exists.`
});
return properties;
}
columns = Object.entries(tableDefinition.properties).map(([columnName, columnDef]: [string, any]) => {
let dataType = 'text';
if (columnDef.type) {
dataType = columnDef.type;
if (columnDef.type === 'array') {
dataType = 'array';
}
else if (columnDef.format) {
if (columnDef.format === 'date-time' || columnDef.format === 'timestamp') {
dataType = 'timestamp';
} else if (columnDef.format === 'date') {
dataType = 'date';
} else if (columnDef.format === 'uuid') {
dataType = 'uuid';
} else if (columnDef.format === 'bigint') {
dataType = 'bigint';
}
}
}
return {
column_name: columnName,
data_type: dataType,
is_nullable: !tableDefinition.required?.includes(columnName) ? 'YES' : 'NO',
column_default: columnDef.default || null
};
});
}
for (const column of columns) {
if (!column.data_type) {
continue;
}
const isRequired = column.is_nullable === 'NO' && column.column_default === null;
const description = `Type: ${column.data_type}${isRequired ? ' (required)' : ''}`;
switch (column.data_type.toLowerCase()) {
case 'integer':
case 'bigint':
case 'smallint':
case 'numeric':
case 'decimal':
case 'real':
case 'double precision':
properties[column.column_name] = Property.Number({
displayName: column.column_name,
description,
required: false
});
break;
case 'boolean':
properties[column.column_name] = Property.Checkbox({
displayName: column.column_name,
description,
required: false
});
break;
case 'date':
case 'timestamp':
case 'timestamp with time zone':
case 'timestamp without time zone':
// Handle auto-timestamps (created_at, updated_at) differently
if (column.column_name.includes('created_at') || column.column_name.includes('updated_at')) {
properties[column.column_name] = Property.ShortText({
displayName: `${column.column_name} (auto-generated)`,
description: `${description} - Leave empty for auto-generation`,
required: false
});
} else {
properties[column.column_name] = Property.DateTime({
displayName: column.column_name,
description,
required: false
});
}
break;
case 'json':
case 'jsonb':
case 'object':
properties[column.column_name] = Property.Json({
displayName: column.column_name,
description,
required: false
});
break;
case 'array':
case '_text':
case 'text[]':
properties[column.column_name] = Property.Array({
displayName: column.column_name,
description: `${description} - Enter each item separately`,
required: false
});
break;
case 'uuid':
// UUID fields - offer auto-generation option
if (column.column_name === 'id' || column.column_name.endsWith('_id')) {
properties[column.column_name] = Property.ShortText({
displayName: `${column.column_name} (auto-generated)`,
description: `${description} - Leave empty for auto-generation`,
required: false
});
} else {
properties[column.column_name] = Property.ShortText({
displayName: column.column_name,
description,
required: false
});
}
break;
case 'string':
case 'text':
case 'varchar':
case 'character varying':
case 'char':
case 'character':
default:
if (column.column_name.toLowerCase().includes('email')) {
properties[column.column_name] = Property.ShortText({
displayName: column.column_name,
description: `${description} - Enter email address`,
required: false
});
} else if (column.column_name.toLowerCase().includes('id')) {
properties[column.column_name] = Property.ShortText({
displayName: `${column.column_name} (auto-generated)`,
description: `${description} - Leave empty for auto-generation`,
required: false
});
} else {
properties[column.column_name] = Property.LongText({
displayName: column.column_name,
description,
required: false
});
}
break;
}
}
return properties;
} catch (error) {
properties['error'] = Property.MarkDown({
value: `Error loading columns for table "${table_name}". Please check your connection and permissions.`
});
return properties;
}
}
}),
update_fields: Property.DynamicProperties({
auth: supabaseAuth,
displayName: 'Update Data',
description: 'Select which columns to update (auto-generated fields excluded)',
required: true,
refreshers: ['table_name'],
props: async (propsValue) => {
const { auth, table_name } = propsValue;
const properties: DynamicPropsValue = {};
if (!auth || !table_name) {
return properties;
}
try {
const { url, apiKey } =auth.props
const supabase = createClient(url, apiKey);
let columns: any[] = [];
try {
const { data: rpcColumns, error } = await supabase.rpc('get_table_columns', {
p_table_name: table_name as unknown as string
});
if (!error && rpcColumns && rpcColumns.length > 0) {
columns = rpcColumns;
}
} catch (rpcError) {
// RPC function doesn't exist, continue to OpenAPI fallback
}
if (columns.length === 0) {
let openApiSpec: any;
try {
const response = await fetch(`${url}/rest/v1/`, {
method: 'GET',
headers: {
'apikey': apiKey,
'Authorization': `Bearer ${apiKey}`,
'Accept': 'application/openapi+json'
}
});
if (!response.ok) {
properties['error'] = Property.MarkDown({
value: `Error loading columns for table "${table_name}". Please check your connection and permissions.`
});
return properties;
}
openApiSpec = await response.json();
} catch (fetchError) {
properties['error'] = Property.MarkDown({
value: `Network error loading columns for table "${table_name}". Please check your connection.`
});
return properties;
}
const definitions = openApiSpec.definitions || openApiSpec.components?.schemas || {};
const tableDefinition = definitions[table_name as unknown as string];
if (!tableDefinition || !tableDefinition.properties) {
properties['info'] = Property.MarkDown({
value: `No columns found for table "${table_name}". Please check if the table exists.`
});
return properties;
}
columns = Object.entries(tableDefinition.properties).map(([columnName, columnDef]: [string, any]) => {
let dataType = 'text';
if (columnDef.type) {
dataType = columnDef.type;
if (columnDef.type === 'array') {
dataType = 'array';
} else if (columnDef.format) {
if (columnDef.format === 'date-time' || columnDef.format === 'timestamp') {
dataType = 'timestamp';
} else if (columnDef.format === 'date') {
dataType = 'date';
} else if (columnDef.format === 'uuid') {
dataType = 'uuid';
} else if (columnDef.format === 'bigint') {
dataType = 'bigint';
}
}
}
return {
column_name: columnName,
data_type: dataType,
is_nullable: !tableDefinition.required?.includes(columnName) ? 'YES' : 'NO',
column_default: columnDef.default || null
};
});
}
for (const column of columns) {
if (!column.data_type) continue;
if (
column.column_name === 'id' ||
column.column_name.includes('created_at') ||
column.column_name.includes('updated_at') ||
(column.data_type === 'uuid' && column.column_name.endsWith('_id'))
) {
continue;
}
const description = `Type: ${column.data_type} - Update this field`;
switch (column.data_type.toLowerCase()) {
case 'integer':
case 'bigint':
case 'smallint':
case 'numeric':
case 'decimal':
case 'real':
case 'double precision':
properties[column.column_name] = Property.Number({
displayName: column.column_name,
description,
required: false
});
break;
case 'boolean':
properties[column.column_name] = Property.Checkbox({
displayName: column.column_name,
description,
required: false
});
break;
case 'date':
case 'timestamp':
case 'timestamp with time zone':
case 'timestamp without time zone':
properties[column.column_name] = Property.DateTime({
displayName: column.column_name,
description,
required: false
});
break;
case 'json':
case 'jsonb':
case 'object':
properties[column.column_name] = Property.Json({
displayName: column.column_name,
description,
required: false
});
break;
case 'array':
case '_text':
case 'text[]':
properties[column.column_name] = Property.Array({
displayName: column.column_name,
description: `${description} - Enter each item separately`,
required: false
});
break;
default:
if (column.column_name.toLowerCase().includes('email')) {
properties[column.column_name] = Property.ShortText({
displayName: column.column_name,
description: `${description} - Enter email address`,
required: false
});
} else {
properties[column.column_name] = Property.LongText({
displayName: column.column_name,
description,
required: false
});
}
break;
}
}
return properties;
} catch (error) {
properties['error'] = Property.MarkDown({
value: `Error loading columns for table "${table_name}". Please check your connection and permissions.`
});
return properties;
}
}
}),
upsert_fields: Property.DynamicProperties({
auth: supabaseAuth,
displayName: 'Row Data',
description: 'Enter data for the row (conflict detection handled separately)',
required: true,
refreshers: ['table_name', 'on_conflict'],
props: async (propsValue) => {
const { auth, table_name, on_conflict } = propsValue;
const properties: DynamicPropsValue = {};
if (!auth || !table_name) {
return properties;
}
try {
const { url, apiKey } =auth.props
const supabase = createClient(url, apiKey);
let columns: any[] = [];
try {
const { data: rpcColumns, error } = await supabase.rpc('get_table_columns', {
p_table_name: table_name as unknown as string
});
if (!error && rpcColumns && rpcColumns.length > 0) {
columns = rpcColumns;
}
} catch (rpcError) {
console.log('RPC function not available for upsert columns');
}
if (columns.length === 0) {
const response = await fetch(`${url}/rest/v1/`, {
method: 'GET',
headers: {
'apikey': apiKey,
'Authorization': `Bearer ${apiKey}`,
'Accept': 'application/openapi+json'
}
});
if (response.ok) {
const openApiSpec = await response.json();
const definitions = openApiSpec.definitions || openApiSpec.components?.schemas || {};
const tableDefinition = definitions[table_name as unknown as string];
if (tableDefinition && tableDefinition.properties) {
columns = Object.entries(tableDefinition.properties).map(([columnName, columnDef]: [string, any]) => ({
column_name: columnName,
data_type: columnDef.type || 'text',
is_nullable: !tableDefinition.required?.includes(columnName) ? 'YES' : 'NO',
column_default: columnDef.default || null
}));
}
}
}
for (const column of columns) {
if (!column.data_type || column.column_name === on_conflict) continue;
const description = `Type: ${column.data_type}`;
properties[column.column_name] = Property.LongText({
displayName: column.column_name,
description,
required: false
});
}
} catch (error) {
properties['error'] = Property.MarkDown({
value: `Error loading columns for table "${table_name}".`
});
}
if (on_conflict) {
properties['_info'] = Property.MarkDown({
value: `💡 **Note**: The "${on_conflict}" field is used for conflict detection and should not be included in the row data unless you want to update it.`
});
}
return properties;
}
})
};

View File

@@ -0,0 +1,106 @@
import { createTrigger, Property, TriggerStrategy } from '@activepieces/pieces-framework';
import { supabaseAuth } from '../../index';
import { supabaseCommon } from '../common/props';
export const newRow = createTrigger({
name: 'new_row',
displayName: 'New Row',
description: 'Fires when a new row is created in a table',
auth: supabaseAuth,
type: TriggerStrategy.WEBHOOK,
sampleData: {
type: "INSERT",
table: "customers",
schema: "public",
record: {
id: 1,
name: "John Doe",
email: "john@example.com",
created_at: "2023-01-01T00:00:00Z"
},
old_record: null
},
props: {
instructions: Property.MarkDown({
value: `## Setup Instructions
1. **Go to your Supabase Dashboard** → Database → Webhooks
2. **Click "Create a new hook"**
3. **Configure the webhook:**
- **Name**: Give it a descriptive name (e.g., "Activepieces New Row")
- **Table**: Select the table you want to monitor
- **Events**: Check "Insert"
- **Type**: HTTP Request
- **Method**: POST
- **URL**: Copy and paste the webhook URL below
4. **Click "Create webhook"**
**Webhook URL:** \`{{webhookUrl}}\`
## Important Notes
- The webhook will send a JSON payload with the new row data
- Make sure your table has the necessary permissions
- You can test the webhook by inserting a new row into your table
For more details, see [Supabase Database Webhooks documentation](https://supabase.com/docs/guides/database/webhooks).`
}),
table_name: supabaseCommon.table_name,
schema: Property.ShortText({
displayName: 'Schema',
description: 'Database schema (default: public)',
required: false,
defaultValue: 'public'
})
},
async onEnable(context) {
const { table_name, schema } = context.propsValue;
if (!context.webhookUrl) {
throw new Error('Webhook URL is required for Supabase triggers');
}
const webhookConfig = {
table: table_name,
schema: schema || 'public',
event: 'INSERT',
webhook_url: context.webhookUrl,
setup_instructions: 'Manual setup required in Supabase Dashboard'
};
await context.store.put('webhook_config', webhookConfig);
},
async onDisable(context) {
try {
await context.store.delete('webhook_config');
} catch (error) {
console.log('Error cleaning up webhook config:', error);
}
},
async run(context) {
const payload = context.payload.body as any;
if (!payload || typeof payload !== 'object') {
throw new Error('Invalid webhook payload received from Supabase');
}
if (!payload.type || !payload.table) {
throw new Error('Payload missing required Supabase webhook fields (type, table)');
}
if (payload.type !== 'INSERT') {
throw new Error(`Expected INSERT event, received ${payload.type}`);
}
return [{
type: payload.type,
table: payload.table,
schema: payload.schema || 'public',
record: payload.record || null,
old_record: payload.old_record || null,
timestamp: new Date().toISOString(),
raw_payload: payload
}];
}
});