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,44 @@
import { createAction } from '@activepieces/pieces-framework';
import { snowflakeAuth } from '../../';
import {
configureConnection,
connect,
destroy,
execute,
snowflakeCommonProps,
} from '../common';
export const insertRowAction = createAction({
name: 'insert-row',
displayName: 'Insert Row',
description: 'Insert a row into a table.',
auth: snowflakeAuth,
props: {
database: snowflakeCommonProps.database,
schema: snowflakeCommonProps.schema,
table: snowflakeCommonProps.table,
table_column_values: snowflakeCommonProps.table_column_values,
},
async run(context) {
const tableName = context.propsValue.table;
const tableColumnValues = context.propsValue.table_column_values;
const columns = Object.keys(tableColumnValues).join(',');
const valuePlaceholders = Object.keys(tableColumnValues)
.map(() => '?')
.join(', ');
const statement = `INSERT INTO ${tableName}(${columns}) VALUES(${valuePlaceholders})`;
const connection = configureConnection(context.auth.props);
await connect(connection);
const response = await execute(
connection,
statement,
Object.values(tableColumnValues)
);
await destroy(connection);
return response;
},
});

View File

@@ -0,0 +1,159 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import snowflake, { Statement, SnowflakeError } from 'snowflake-sdk';
import { snowflakeAuth } from '../../index';
import { configureConnection } from '../common';
type QueryResult = unknown[] | undefined;
type QueryResults = { query: string; result: QueryResult }[];
const DEFAULT_APPLICATION_NAME = 'ActivePieces';
const DEFAULT_QUERY_TIMEOUT = 30000;
export const runMultipleQueries = createAction({
name: 'runMultipleQueries',
displayName: 'Run Multiple Queries',
description: 'Run Multiple Queries',
auth: snowflakeAuth,
props: {
sqlTexts: Property.Array({
displayName: 'SQL queries',
description:
'Array of SQL queries to execute in order, in the same transaction. Use :1, :2… placeholders to use binding parameters. ' +
'Avoid using "?" to avoid unexpected behaviors when having multiple queries.',
required: true,
}),
binds: Property.Array({
displayName: 'Parameters',
description:
'Binding parameters shared across all queries to prevent SQL injection attacks. ' +
'Use :1, :2, etc. to reference parameters in order. ' +
'Avoid using "?" to avoid unexpected behaviors when having multiple queries. ' +
'Unused parameters are allowed.',
required: false,
}),
useTransaction: Property.Checkbox({
displayName: 'Use Transaction',
description:
'When enabled, all queries will be executed in a single transaction. If any query fails, all changes will be rolled back.',
required: false,
defaultValue: false,
}),
timeout: Property.Number({
displayName: 'Query timeout (ms)',
description:
'An integer indicating the maximum number of milliseconds to wait for a query to complete before timing out.',
required: false,
defaultValue: DEFAULT_QUERY_TIMEOUT,
}),
application: Property.ShortText({
displayName: 'Application name',
description:
'A string indicating the name of the client application connecting to the server.',
required: false,
defaultValue: DEFAULT_APPLICATION_NAME,
}),
},
async run(context) {
const connection = configureConnection(
context.auth.props,
context.propsValue.application,
context.propsValue.timeout
);
return new Promise<QueryResults>((resolve, reject) => {
connection.connect(async function (err: SnowflakeError | undefined) {
if (err) {
reject(err);
return;
}
const { sqlTexts, binds, useTransaction } = context.propsValue;
const queryResults: QueryResults = [];
function handleError(err: SnowflakeError) {
if (useTransaction) {
connection.execute({
sqlText: 'ROLLBACK',
complete: () => {
connection.destroy(() => {
reject(err);
});
},
});
} else {
connection.destroy(() => {
reject(err);
});
}
}
async function executeQueriesSequentially() {
try {
if (useTransaction) {
await new Promise<void>((resolveBegin, rejectBegin) => {
connection.execute({
sqlText: 'BEGIN',
complete: (err: SnowflakeError | undefined) => {
if (err) rejectBegin(err);
else resolveBegin();
},
});
});
}
for (const sqlText of sqlTexts) {
const result = await new Promise<QueryResult>(
(resolveQuery, rejectQuery) => {
connection.execute({
sqlText: sqlText as string,
binds: binds as snowflake.Binds,
complete: (
err: SnowflakeError | undefined,
stmt: Statement,
rows: QueryResult
) => {
if (err) {
rejectQuery(err);
return;
}
resolveQuery(rows);
},
});
}
);
queryResults.push({
query: sqlText as string,
result,
});
}
if (useTransaction) {
await new Promise<void>((resolveCommit, rejectCommit) => {
connection.execute({
sqlText: 'COMMIT',
complete: (err: SnowflakeError | undefined) => {
if (err) rejectCommit(err);
else resolveCommit();
},
});
});
}
connection.destroy((err: SnowflakeError | undefined) => {
if (err) {
reject(err);
return;
}
resolve(queryResults);
});
} catch (err) {
handleError(err as SnowflakeError); // Reject with the original error!
}
}
executeQueriesSequentially();
});
});
},
});

View File

@@ -0,0 +1,74 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import snowflake from 'snowflake-sdk';
import { snowflakeAuth } from '../../index';
import { configureConnection } from '../common';
const DEFAULT_APPLICATION_NAME = 'ActivePieces';
const DEFAULT_QUERY_TIMEOUT = 30000;
export const runQuery = createAction({
name: 'runQuery',
displayName: 'Run Query',
description: 'Run Query',
auth: snowflakeAuth,
props: {
sqlText: Property.ShortText({
displayName: 'SQL query',
description: 'Use :1, :2… or ? placeholders to use binding parameters.',
required: true,
}),
binds: Property.Array({
displayName: 'Parameters',
description:
'Binding parameters for the SQL query (to prevent SQL injection attacks)',
required: false,
}),
timeout: Property.Number({
displayName: 'Query timeout (ms)',
description:
'An integer indicating the maximum number of milliseconds to wait for a query to complete before timing out.',
required: false,
defaultValue: DEFAULT_QUERY_TIMEOUT,
}),
application: Property.ShortText({
displayName: 'Application name',
description:
'A string indicating the name of the client application connecting to the server.',
required: false,
defaultValue: DEFAULT_APPLICATION_NAME,
}),
},
async run(context) {
const connection = configureConnection(
context.auth.props,
context.propsValue.application,
context.propsValue.timeout
);
return new Promise((resolve, reject) => {
connection.connect(function (err, conn) {
if (err) {
reject(err);
}
});
const { sqlText, binds } = context.propsValue;
connection.execute({
sqlText,
binds: binds as snowflake.Binds,
complete: (err, stmt, rows) => {
if (err) {
reject(err);
}
connection.destroy((err, conn) => {
if (err) {
reject(err);
}
});
resolve(rows);
},
});
});
},
});

View File

@@ -0,0 +1,266 @@
import {
DynamicPropsValue,
PiecePropValueSchema,
Property,
} from '@activepieces/pieces-framework';
import { snowflakeAuth } from '../..';
import snowflake from 'snowflake-sdk';
const DEFAULT_APPLICATION_NAME = 'ActivePieces';
const DEFAULT_QUERY_TIMEOUT = 30000;
function formatPrivateKey(privateKey: string): string {
const privateKeyLines = privateKey
.replace('-----BEGIN PRIVATE KEY-----', '')
.replace('-----END PRIVATE KEY-----', '')
.trim()
.split(' ');
return [
'-----BEGIN PRIVATE KEY-----',
...privateKeyLines,
'-----END PRIVATE KEY-----',
].join('\n');
}
export function configureConnection(
auth: PiecePropValueSchema<typeof snowflakeAuth>,
application = DEFAULT_APPLICATION_NAME,
timeout = DEFAULT_QUERY_TIMEOUT
) {
const connectionOptions: snowflake.ConnectionOptions = {
application: application,
timeout: timeout,
username: auth.username,
role: auth.role,
database: auth.database,
warehouse: auth.warehouse,
account: auth.account,
};
if (auth.privateKey) {
connectionOptions.privateKey = formatPrivateKey(auth.privateKey);
connectionOptions.authenticator = 'SNOWFLAKE_JWT';
} else {
connectionOptions.password = auth.password;
}
return snowflake.createConnection(connectionOptions);
}
export async function connect(conn: snowflake.Connection) {
return await new Promise<void>((resolve, reject) => {
conn.connect((error) => {
if (error) {
reject(error);
} else {
resolve();
}
});
});
}
export async function destroy(conn: snowflake.Connection) {
return await new Promise<void>((resolve, reject) => {
conn.destroy((error) => {
if (error) {
reject(error);
} else {
resolve();
}
});
});
}
export async function execute(
conn: snowflake.Connection,
sqlText: string,
binds: snowflake.Binds
) {
return await new Promise<any[] | undefined>((resolve, reject) => {
conn.execute({
sqlText,
binds,
complete: (error, _, result) => {
if (error) {
reject(error);
} else {
resolve(result);
}
},
});
});
}
export const snowflakeCommonProps = {
database: Property.Dropdown({
auth: snowflakeAuth,
displayName: 'Database',
refreshers: [],
required: true,
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
options: [],
placeholder: 'Please connect your account first',
};
}
const authValue = auth;
const connection = configureConnection(authValue.props);
await connect(connection);
const response = await execute(connection, 'SHOW DATABASES', []);
await destroy(connection);
return {
disabled: false,
options: response
? response.map((db: any) => {
return {
label: db.name,
value: db.name,
};
})
: [],
};
},
}),
schema: Property.Dropdown({
auth: snowflakeAuth,
displayName: 'Schema',
refreshers: ['database'],
required: true,
options: async ({ auth, database }) => {
if (!auth) {
return {
disabled: true,
options: [],
placeholder: 'Please connect your account first',
};
}
if (!database) {
return {
disabled: true,
options: [],
placeholder: 'Please select database first',
};
}
const authValue = auth;
const connection = configureConnection(authValue.props);
await connect(connection);
const response = await execute(
connection,
`SHOW SCHEMAS IN DATABASE ${database}`,
[]
);
await destroy(connection);
return {
disabled: false,
options: response
? response.map((schema: any) => {
return {
label: schema.name,
value: schema.name,
};
})
: [],
};
},
}),
table: Property.Dropdown({
auth: snowflakeAuth,
displayName: 'Table',
refreshers: ['database', 'schema'],
required: true,
options: async ({ auth, database, schema }) => {
if (!auth) {
return {
disabled: true,
options: [],
placeholder: 'Please connect your account first',
};
}
if (!database) {
return {
disabled: true,
options: [],
placeholder: 'Please select database first',
};
}
if (!schema) {
return {
disabled: true,
options: [],
placeholder: 'Please select schema first',
};
}
const authValue = auth;
const connection = configureConnection(authValue.props);
await connect(connection);
const response = await execute(
connection,
`SHOW TABLES IN SCHEMA ${database}.${schema}`,
[]
);
await destroy(connection);
return {
disabled: false,
options: response
? response.map((table: any) => {
return {
label: table.name,
value: `${database}.${schema}.${table.name}`,
};
})
: [],
};
},
}),
table_column_values: Property.DynamicProperties({
auth: snowflakeAuth,
displayName: 'Rows',
required: true,
refreshers: ['database', 'schema', 'table'],
props: async ({ auth, table }) => {
if (!auth) return {};
if (!table) return {};
const authValue = auth;
const connection = configureConnection(authValue.props);
await connect(connection);
const response = await execute(connection, `DESCRIBE TABLE ${table}`, []);
await destroy(connection);
const fields: DynamicPropsValue = {};
if (response) {
for (const column of response) {
fields[column.name] = Property.ShortText({
displayName: column.name,
required: false,
});
}
}
return fields;
},
}),
};