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,59 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { surrealdbAuth } from '../..';
import surrealClient from '../common';
export const runQuery = createAction({
auth: surrealdbAuth,
name: 'run-query',
displayName: 'Run Query',
description: 'Run a query in SurrealDB.',
props: {
markdown: Property.MarkDown({
value: `
**NOTE:** Prevent SQL injection by using parameterized queries.
`,
}),
query: Property.ShortText({
displayName: 'Query',
description: 'Provide a SurrealDB query string to execute.',
required: true,
}),
queryMarkdown: Property.MarkDown({
value: `
**NOTE:** Query example: \`SELECT * FROM table_name WHERE name = $name\`. Then add the name parameter in the arguments.
`,
}),
args: Property.Object({
displayName: 'Arguments',
description: "Add all arguments as names here, don't add the $ sign.",
required: false,
}),
query_timeout: Property.Number({
displayName: 'Query Timeout (ms)',
description:
'The maximum time to wait for a query to complete before timing out.',
required: false,
defaultValue: 30000,
}),
application_name: Property.ShortText({
displayName: 'Application Name',
description:
'An identifier for the client application executing the query.',
required: false,
}),
},
async run(context) {
try {
const { query, args } = context.propsValue;
const response = await surrealClient.query(
context.auth.props,
query,
args as Record<string, string>
);
return response.body;
} catch (error) {
throw new Error(`Query execution failed: ${(error as Error).message}`);
}
},
});

View File

@@ -0,0 +1,40 @@
import {
HttpMethod,
HttpRequest,
httpClient,
} from '@activepieces/pieces-common';
import { PiecePropValueSchema } from '@activepieces/pieces-framework';
import { surrealdbAuth } from '..';
const query = async (
auth: PiecePropValueSchema<typeof surrealdbAuth>,
query: string,
args?: Record<string, string>
) => {
const { url, username, password, namespace, database } = auth;
const sqlUrl = new URL('/sql', url);
const request: HttpRequest = {
method: HttpMethod.POST,
url: sqlUrl.toString(),
headers: {
'Content-Type': 'text/plain',
Accept: 'application/json',
Authorization: `Basic ${Buffer.from(`${username}:${password}`).toString(
'base64'
)}`,
'surreal-ns': namespace,
'surreal-db': database,
},
queryParams: args,
body: query,
};
const response = await httpClient.sendRequest(request);
return response;
};
export default {
query,
};

View File

@@ -0,0 +1,192 @@
import {
createTrigger,
TriggerStrategy,
PiecePropValueSchema,
Property,
AppConnectionValueForAuthProperty,
} from '@activepieces/pieces-framework';
import {
DedupeStrategy,
Polling,
pollingHelper,
} from '@activepieces/pieces-common';
import dayjs from 'dayjs';
import { surrealdbAuth } from '../..';
import client from '../common';
import crypto from 'crypto';
// replace auth with piece auth variable
const polling: Polling<
AppConnectionValueForAuthProperty<typeof surrealdbAuth>,
{
table: string;
order_by: string;
order_direction: 'ASC' | 'DESC' | undefined;
}
> = {
strategy: DedupeStrategy.LAST_ITEM,
items: async ({ auth, propsValue, lastItemId }) => {
const lastItem = lastItemId as string;
const query = constructQuery({
order_by: propsValue.order_by,
lastItem: lastItem,
order_direction: propsValue.order_direction,
});
const authProps = auth.props;
const result = await client.query(authProps, query, {
table: propsValue.table,
});
const items = result.body[0].result.map(function (
row: Record<string, any>
) {
const rowHash = crypto
.createHash('md5')
.update(JSON.stringify(row))
.digest('hex');
const isTimestamp = dayjs(row[propsValue.order_by]).isValid();
const orderValue = isTimestamp
? dayjs(row[propsValue.order_by]).toISOString()
: row[propsValue.order_by];
return {
id: orderValue + '|' + rowHash,
data: row,
};
});
return items;
},
};
function constructQuery({
order_by,
lastItem,
order_direction,
}: {
order_by: string;
order_direction: 'ASC' | 'DESC' | undefined;
lastItem: string;
}): string {
const lastOrderKey = lastItem ? lastItem.split('|')[0] : null;
if (lastOrderKey === null) {
switch (order_direction) {
case 'ASC':
return `SELECT * FROM type::table($table) ORDER BY ${order_by} ASC LIMIT 5`;
case 'DESC':
return `SELECT * FROM type::table($table) ORDER BY ${order_by} DESC LIMIT 5`;
default:
throw new Error(
JSON.stringify({
message: 'Invalid order direction',
order_direction: order_direction,
})
);
}
} else {
switch (order_direction) {
case 'ASC':
return `SELECT * FROM type::table($table) WHERE ${order_by} <= '${lastOrderKey}' ORDER BY ${order_by} ASC`;
case 'DESC':
return `SELECT * FROM type::table($table) WHERE ${order_by} >= '${lastOrderKey}' ORDER BY ${order_by} DESC`;
default:
throw new Error(
JSON.stringify({
message: 'Invalid order direction',
order_direction: order_direction,
})
);
}
}
}
export const newRow = createTrigger({
name: 'new-row',
displayName: 'New Row',
description: 'Triggers when a new row is added to the defined table.',
props: {
description: Property.MarkDown({
value: `**NOTE:** The trigger fetches the latest rows using the provided order by column (newest first), and then will keep polling until the previous last row is reached. It's suggested to add a created_at timestamp. \`DEFINE FIELD OVERWRITE createdAt ON schedule VALUE time::now() READONLY;\``,
}),
table: Property.Dropdown({
displayName: 'Table name',
required: true,
refreshers: ['auth'],
refreshOnSearch: false,
auth: surrealdbAuth,
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
options: [],
placeholder: 'Please authenticate first',
};
}
const authProps = auth.props;
try {
const result = await client.query(authProps, 'INFO FOR DB');
const options = Object.keys(result.body[0].result.tables).map(
(row) => ({
label: row,
value: row,
})
);
return {
disabled: false,
options,
};
} catch (e) {
return {
disabled: true,
options: [],
placeholder: JSON.stringify(e),
};
}
},
}),
order_by: Property.ShortText({
displayName: 'Column to order by',
description: 'Use something like a created timestamp.',
required: true,
defaultValue: 'created_at',
}),
order_direction: Property.StaticDropdown<'ASC' | 'DESC'>({
displayName: 'Order Direction',
description:
'The direction to sort by such that the newest rows are fetched first.',
required: true,
options: {
options: [
{
label: 'Ascending',
value: 'ASC',
},
{
label: 'Descending',
value: 'DESC',
},
],
},
defaultValue: 'DESC',
}),
},
sampleData: {},
type: TriggerStrategy.POLLING,
auth: surrealdbAuth,
async test(context) {
return await pollingHelper.test(polling, context);
},
async onEnable(context) {
const { store, auth, propsValue } = context;
await pollingHelper.onEnable(polling, { store, propsValue, auth });
},
async onDisable(context) {
const { store, auth, propsValue } = context;
await pollingHelper.onDisable(polling, { store, propsValue, auth });
},
async run(context) {
return await pollingHelper.poll(polling, context);
},
});