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:
@@ -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;
|
||||
},
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
@@ -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
|
||||
}
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
@@ -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,
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
})
|
||||
};
|
||||
@@ -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
|
||||
}];
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user