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,18 @@
{
"extends": ["../../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {}
},
{
"files": ["*.ts", "*.tsx"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"rules": {}
}
]
}

View File

@@ -0,0 +1,7 @@
# cli
This library was generated with [Nx](https://nx.dev).
## Running unit tests
Run `nx test cli` to execute the unit tests via [Jest](https://jestjs.io).

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,11 @@
/* eslint-disable */
export default {
displayName: 'cli',
preset: '../../jest.preset.js',
testEnvironment: 'node',
transform: {
'^.+\\.[tj]s$': ['ts-jest', { tsconfig: '<rootDir>/tsconfig.spec.json' }],
},
moduleFileExtensions: ['ts', 'js', 'html'],
coverageDirectory: '../../coverage/packages/cli',
};

View File

@@ -0,0 +1,16 @@
{
"name": "@activepieces/cli",
"version": "1.0.1",
"bin": {
"pieces-cli":"../../dist/cli/src/index.js"
},
"scripts": {
"start": "ts-node src/index.ts",
"build": "tsc"
},
"dependencies": {
"@activepieces/pieces-framework": "*",
"@activepieces/shared": "*"
}
}

View File

@@ -0,0 +1,29 @@
{
"name": "cli",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "packages/cli/src",
"projectType": "library",
"tags": [],
"targets": {
"build": {
"executor": "@nx/js:tsc",
"outputs": ["{options.outputPath}"],
"options": {
"outputPath": "dist/packages/cli",
"main": "packages/cli/src/index.ts",
"tsConfig": "packages/cli/tsconfig.lib.json"
}
},
"lint": {
"executor": "@nx/eslint:lint",
"outputs": ["{options.outputFile}"]
},
"test": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "packages/cli/jest.config.ts"
}
}
}
}

View File

@@ -0,0 +1,44 @@
import { Command } from 'commander';
import { createActionCommand } from './lib/commands/create-action';
import { createPieceCommand } from './lib/commands/create-piece';
import { createTriggerCommand } from './lib/commands/create-trigger';
import { syncPieceCommand } from './lib/commands/sync-pieces';
import { publishPieceCommand } from './lib/commands/publish-piece';
import { buildPieceCommand } from './lib/commands/build-piece';
import { generateWorkerTokenCommand } from './lib/commands/generate-worker-token';
import { generateTranslationFileForAllPiecesCommand, generateTranslationFileForPieceCommand } from './lib/commands/generate-translation-file-for-piece';
const pieceCommand = new Command('pieces')
.description('Manage pieces');
pieceCommand.addCommand(createPieceCommand);
pieceCommand.addCommand(syncPieceCommand);
pieceCommand.addCommand(publishPieceCommand);
pieceCommand.addCommand(buildPieceCommand);
pieceCommand.addCommand(generateTranslationFileForPieceCommand);
pieceCommand.addCommand(generateTranslationFileForAllPiecesCommand);
const actionCommand = new Command('actions')
.description('Manage actions');
actionCommand.addCommand(createActionCommand);
const triggerCommand = new Command('triggers')
.description('Manage triggers')
triggerCommand.addCommand(createTriggerCommand)
const workerCommand = new Command('workers')
.description('Manage workers')
workerCommand.addCommand(generateWorkerTokenCommand)
const program = new Command();
program.version('0.0.1').description('Activepieces CLI');
program.addCommand(pieceCommand);
program.addCommand(actionCommand);
program.addCommand(triggerCommand);
program.addCommand(workerCommand);
program.parse(process.argv);

View File

@@ -0,0 +1,29 @@
import { Command } from "commander";
import { buildPiece, findPiece } from '../utils/piece-utils';
import chalk from "chalk";
import inquirer from "inquirer";
async function buildPieces(pieceName: string) {
const pieceFolder = await findPiece(pieceName);
const { outputFolder } = await buildPiece(pieceFolder);
console.info(chalk.green(`Piece '${pieceName}' built and packed successfully at ${outputFolder}.`));
}
export const buildPieceCommand = new Command('build')
.description('Build pieces without publishing')
.argument('[name]', 'name of the piece to build')
.action(async (name) => {
const questions = [
{
type: 'input',
name: 'name',
message: 'Enter the piece folder name',
placeholder: 'google-drive',
when() {
return !name
}
},
];
const answers = await inquirer.prompt(questions);
await buildPieces(name ? name : answers.name);
});

View File

@@ -0,0 +1,74 @@
import { writeFile } from 'node:fs/promises';
import chalk from 'chalk';
import { Command } from 'commander';
import inquirer from 'inquirer';
import { assertPieceExists, displayNameToCamelCase, displayNameToKebabCase, findPiece } from '../utils/piece-utils';
import { checkIfFileExists, makeFolderRecursive } from '../utils/files';
import { join } from 'node:path';
function createActionTemplate(displayName: string, description: string) {
const camelCase = displayNameToCamelCase(displayName)
const actionTemplate = `import { createAction, Property } from '@activepieces/pieces-framework';
export const ${camelCase} = createAction({
// auth: check https://www.activepieces.com/docs/developers/piece-reference/authentication,
name: '${camelCase}',
displayName: '${displayName}',
description: '${description}',
props: {},
async run() {
// Action logic here
},
});
`;
return actionTemplate
}
const checkIfActionExists = async (actionPath: string) => {
if (await checkIfFileExists(actionPath)) {
console.log(chalk.red(`🚨 Action already exists at ${actionPath}`));
process.exit(1);
}
}
const createAction = async (pieceName: string, displayActionName: string, actionDescription: string) => {
const actionTemplate = createActionTemplate(displayActionName, actionDescription)
const actionName = displayNameToKebabCase(displayActionName)
const pieceFolder = await findPiece(pieceName);
assertPieceExists(pieceFolder)
console.log(chalk.blue(`Piece path: ${pieceFolder}`))
const actionsFolder = join(pieceFolder, 'src', 'lib', 'actions')
const actionPath = join(actionsFolder, `${actionName}.ts`)
await checkIfActionExists(actionPath)
await makeFolderRecursive(actionsFolder);
await writeFile(actionPath, actionTemplate);
console.log(chalk.yellow('✨'), `Action ${actionPath} created`);
};
export const createActionCommand = new Command('create')
.description('Create a new action')
.action(async () => {
const questions = [
{
type: 'input',
name: 'pieceName',
message: 'Enter the piece folder name:',
placeholder: 'google-drive',
},
{
type: 'input',
name: 'actionName',
message: 'Enter the action display name',
},
{
type: 'input',
name: 'actionDescription',
message: 'Enter the action description',
}
];
const answers = await inquirer.prompt(questions);
createAction(answers.pieceName, answers.actionName, answers.actionDescription);
});

View File

@@ -0,0 +1,247 @@
import chalk from 'chalk';
import { Command } from 'commander';
import { readdir, unlink, writeFile } from 'fs/promises';
import inquirer from 'inquirer';
import assert from 'node:assert';
import { exec } from '../utils/exec';
import {
readPackageEslint,
readProjectJson,
writePackageEslint,
writeProjectJson,
} from '../utils/files';
import { findPiece } from '../utils/piece-utils';
const validatePieceName = async (pieceName: string) => {
console.log(chalk.yellow('Validating piece name....'));
const pieceNamePattern = /^(?![._])[a-z0-9-]{1,214}$/;
if (!pieceNamePattern.test(pieceName)) {
console.log(
chalk.red(
`🚨 Invalid piece name: ${pieceName}. Piece names can only contain lowercase letters, numbers, and hyphens.`
)
);
process.exit(1);
}
};
const validatePackageName = async (packageName: string) => {
console.log(chalk.yellow('Validating package name....'));
const packageNamePattern = /^(?:@[a-zA-Z0-9-]+\/)?[a-zA-Z0-9-]+$/;
if (!packageNamePattern.test(packageName)) {
console.log(
chalk.red(
`🚨 Invalid package name: ${packageName}. Package names can only contain lowercase letters, numbers, and hyphens.`
)
);
process.exit(1);
}
};
const checkIfPieceExists = async (pieceName: string) => {
const pieceFolder = await findPiece(pieceName);
if (pieceFolder) {
console.log(chalk.red(`🚨 Piece already exists at ${pieceFolder}`));
process.exit(1);
}
};
const nxGenerateNodeLibrary = async (
pieceName: string,
packageName: string,
pieceType: string
) => {
const nxGenerateCommand = [
`npx nx generate @nx/node:library`,
`--directory=packages/pieces/${pieceType}/${pieceName}`,
`--name=pieces-${pieceName}`,
`--importPath=${packageName}`,
'--publishable',
'--buildable',
'--projectNameAndRootFormat=as-provided',
'--strict',
'--unitTestRunner=none',
].join(' ');
console.log(chalk.blue(`🛠️ Executing nx command: ${nxGenerateCommand}`));
await exec(nxGenerateCommand);
};
const removeUnusedFiles = async (pieceName: string, pieceType: string) => {
const path = `packages/pieces/${pieceType}/${pieceName}/src/lib/`;
const files = await readdir(path);
for (const file of files) {
await unlink(path + file);
}
};
function capitalizeFirstLetter(str: string): string {
return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
}
const generateIndexTsFile = async (pieceName: string, pieceType: string) => {
const pieceNameCamelCase = pieceName
.split('-')
.map((s, i) => {
if (i === 0) {
return s;
}
return s[0].toUpperCase() + s.substring(1);
})
.join('');
const indexTemplate = `
import { createPiece, PieceAuth } from "@activepieces/pieces-framework";
export const ${pieceNameCamelCase} = createPiece({
displayName: "${capitalizeFirstLetter(pieceName)}",
auth: PieceAuth.None(),
minimumSupportedRelease: '0.36.1',
logoUrl: "https://cdn.activepieces.com/pieces/${pieceName}.png",
authors: [],
actions: [],
triggers: [],
});
`;
await writeFile(
`packages/pieces/${pieceType}/${pieceName}/src/index.ts`,
indexTemplate
);
};
const updateProjectJsonConfig = async (
pieceName: string,
pieceType: string
) => {
const projectJson = await readProjectJson(
`packages/pieces/${pieceType}/${pieceName}`
);
const i18nAsset = {
input: `packages/pieces/${pieceType}/${pieceName}/src/i18n`,
output: './src/i18n',
glob: '**/!(i18n.json)'
}
assert(
projectJson.targets?.build?.options,
'[updateProjectJsonConfig] targets.build.options is required'
);
projectJson.targets.build.dependsOn = ['prebuild', '^build'];
projectJson.targets.prebuild = {
dependsOn: ['^build'],
executor: 'nx:run-commands',
options: {
cwd: `packages/pieces/${pieceType}/${pieceName}`,
command: 'bun install --no-save --silent'
}
};
projectJson.targets.build.options.buildableProjectDepsInPackageJsonType =
'dependencies';
projectJson.targets.build.options.updateBuildableProjectDepsInPackageJson =
true;
if(projectJson.targets.build.options.assets){
projectJson.targets.build.options.assets.push(i18nAsset);
}
else{
projectJson.targets.build.options.assets = [i18nAsset];
}
const lintFilePatterns = projectJson.targets.lint?.options?.lintFilePatterns;
if (lintFilePatterns) {
const patternIndex = lintFilePatterns.findIndex((item) =>
item.endsWith('package.json')
);
if (patternIndex !== -1) lintFilePatterns?.splice(patternIndex, 1);
} else {
projectJson.targets.lint = {
executor: '@nx/eslint:lint',
outputs: ['{options.outputFile}'],
};
}
await writeProjectJson(
`packages/pieces/${pieceType}/${pieceName}`,
projectJson
);
};
const addEslintFile = async (pieceName: string, pieceType: string) => {
const eslintFile ={
"extends": ["../../../../.eslintrc.base.json"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {}
},
{
"files": ["*.ts", "*.tsx"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"rules": {}
}
]
}
await writePackageEslint(
`packages/pieces/${pieceType}/${pieceName}`,
eslintFile
);
};
const setupGeneratedLibrary = async (pieceName: string, pieceType: string) => {
await removeUnusedFiles(pieceName, pieceType);
await generateIndexTsFile(pieceName, pieceType);
await updateProjectJsonConfig(pieceName, pieceType);
await addEslintFile(pieceName, pieceType);
};
export const createPiece = async (
pieceName: string,
packageName: string,
pieceType: string
) => {
await validatePieceName(pieceName);
await validatePackageName(packageName);
await checkIfPieceExists(pieceName);
await nxGenerateNodeLibrary(pieceName, packageName, pieceType);
await setupGeneratedLibrary(pieceName, pieceType);
console.log(chalk.green('✨ Done!'));
console.log(
chalk.yellow(
`The piece has been generated at: packages/pieces/${pieceType}/${pieceName}`
)
);
};
export const createPieceCommand = new Command('create')
.description('Create a new piece')
.action(async () => {
const questions = [
{
type: 'input',
name: 'pieceName',
message: 'Enter the piece name:',
},
{
type: 'input',
name: 'packageName',
message: 'Enter the package name:',
default: (answers: any) => `@activepieces/piece-${answers.pieceName}`,
when: (answers: any) => answers.pieceName !== undefined,
},
{
type: 'list',
name: 'pieceType',
message: 'Select the piece type:',
choices: ['community', 'custom'],
default: 'community',
},
];
const answers = await inquirer.prompt(questions);
createPiece(answers.pieceName, answers.packageName, answers.pieceType);
});

View File

@@ -0,0 +1,141 @@
import chalk from 'chalk';
import { Command } from 'commander';
import inquirer from 'inquirer';
import { writeFile } from 'node:fs/promises';
import { join } from 'node:path';
import { checkIfFileExists, makeFolderRecursive } from '../utils/files';
import {
assertPieceExists,
displayNameToCamelCase,
displayNameToKebabCase, findPiece,
} from '../utils/piece-utils';
function createTriggerTemplate(displayName: string, description: string, technique: string) {
const camelCase = displayNameToCamelCase(displayName)
let triggerTemplate = ''
if (technique === 'polling') {
triggerTemplate = `
import { createTrigger, TriggerStrategy, AppConnectionValueForAuthProperty } from '@activepieces/pieces-framework';
import { DedupeStrategy, Polling, pollingHelper } from '@activepieces/pieces-common';
import dayjs from 'dayjs';
// replace auth with piece auth variable
const polling: Polling<AppConnectionValueForAuthProperty<undefined>, Record<string, never> > = {
strategy: DedupeStrategy.TIMEBASED,
items: async ({ propsValue, lastFetchEpochMS }) => {
// implement the logic to fetch the items
const items = [ {id: 1, created_date: '2021-01-01T00:00:00Z'}, {id: 2, created_date: '2021-01-01T00:00:00Z'}];
return items.map((item) => ({
epochMilliSeconds: dayjs(item.created_date).valueOf(),
data: item,
}));
}
}
export const ${camelCase} = createTrigger({
// auth: check https://www.activepieces.com/docs/developers/piece-reference/authentication,
name: '${camelCase}',
displayName: '${displayName}',
description: '${description}',
props: {},
sampleData: {},
type: TriggerStrategy.POLLING,
async test(context) {
return await pollingHelper.test(polling, context);
},
async onEnable(context) {
const { store, auth, propsValue } = context;
await pollingHelper.onEnable(polling, { store, auth, propsValue });
},
async onDisable(context) {
const { store, auth, propsValue } = context;
await pollingHelper.onDisable(polling, { store, auth, propsValue });
},
async run(context) {
return await pollingHelper.poll(polling, context);
},
});`;
}
else {
triggerTemplate = `
import { createTrigger, TriggerStrategy } from '@activepieces/pieces-framework';
export const ${camelCase} = createTrigger({
// auth: check https://www.activepieces.com/docs/developers/piece-reference/authentication,
name: '${camelCase}',
displayName: '${displayName}',
description: '${description}',
props: {},
sampleData: {},
type: TriggerStrategy.WEBHOOK,
async onEnable(context){
// implement webhook creation logic
},
async onDisable(context){
// implement webhook deletion logic
},
async run(context){
return [context.payload.body]
}
})`;
}
return triggerTemplate
}
const checkIfTriggerExists = async (triggerPath: string) => {
if (await checkIfFileExists(triggerPath)) {
console.log(chalk.red(`🚨 Trigger already exists at ${triggerPath}`));
process.exit(1);
}
}
const createTrigger = async (pieceName: string, displayTriggerName: string, triggerDescription: string, triggerTechnique: string) => {
const triggerTemplate = createTriggerTemplate(displayTriggerName, triggerDescription, triggerTechnique)
const triggerName = displayNameToKebabCase(displayTriggerName)
const pieceFolder = await findPiece(pieceName);
assertPieceExists(pieceFolder)
console.log(chalk.blue(`Piece path: ${pieceFolder}`))
const triggersFolder = join(pieceFolder, 'src', 'lib', 'triggers')
const triggerPath = join(triggersFolder, `${triggerName}.ts`)
await checkIfTriggerExists(triggerPath)
await makeFolderRecursive(triggersFolder);
await writeFile(triggerPath, triggerTemplate);
console.log(chalk.yellow('✨'), `Trigger ${triggerPath} created`);
};
export const createTriggerCommand = new Command('create')
.description('Create a new trigger')
.action(async () => {
const questions = [
{
type: 'input',
name: 'pieceName',
message: 'Enter the piece folder name:',
placeholder: 'google-drive',
},
{
type: 'input',
name: 'triggerName',
message: 'Enter the trigger display name:',
},
{
type: 'input',
name: 'triggerDescription',
message: 'Enter the trigger description:',
},
{
type: 'list',
name: 'triggerTechnique',
message: 'Select the trigger technique:',
choices: ['polling', 'webhook'],
default: 'webhook',
},
];
const answers = await inquirer.prompt(questions);
createTrigger(answers.pieceName, answers.triggerName, answers.triggerDescription, answers.triggerTechnique);
});

View File

@@ -0,0 +1,112 @@
import { writeFile } from 'node:fs/promises';
import chalk from 'chalk';
import { Command } from 'commander';
import { buildPiece, findPiece, findPieces } from '../utils/piece-utils';
import { makeFolderRecursive } from '../utils/files';
import { join, basename } from 'node:path';
import { exec } from '../utils/exec';
import { pieceTranslation } from '@activepieces/pieces-framework';
import { MAX_KEY_LENGTH_FOR_CORWDIN } from '@activepieces/shared';
const findPieceInModule= async (pieceOutputFile: string) => {
const module = await import(pieceOutputFile);
const exports = Object.values(module);
for (const e of exports) {
if (e !== null && e !== undefined && e.constructor.name === 'Piece') {
return e
}
}
throw new Error(`Piece not found in module, please check the piece output file ${pieceOutputFile}`);
}
const installDependencies = async (pieceFolder: string) => {
console.log(chalk.blue(`Installing dependencies ${pieceFolder}`))
await exec(`bun install`, {cwd: pieceFolder,})
console.log(chalk.green(`Dependencies installed ${pieceFolder}`))
}
function getPropertyValue(object: Record<string, unknown>, path: string): unknown {
const parsedKeys = path.split('.');
if (parsedKeys[0] === '*') {
return Object.values(object).map(item => getPropertyValue(item as Record<string, unknown>, parsedKeys.slice(1).join('.'))).filter(Boolean).flat()
}
const nextObject = object[parsedKeys[0]] as Record<string, unknown>;
if (nextObject && parsedKeys.length > 1) {
return getPropertyValue(nextObject, parsedKeys.slice(1).join('.'));
}
return nextObject;
}
const generateTranslationFileFromPiece = (piece: Record<string, unknown>) => { const translation: Record<string, string> = {}
try {
pieceTranslation.pathsToValuesToTranslate.forEach(path => {
const value = getPropertyValue(piece, path)
if (value) {
if (typeof value === 'string') {
translation[value.slice(0, MAX_KEY_LENGTH_FOR_CORWDIN)] = value
}
else if (Array.isArray(value)) {
value.forEach(item => {
translation[item.slice(0, MAX_KEY_LENGTH_FOR_CORWDIN)] = item
})
}
}
})
}
catch (err) {
console.error(`error generating translation file for piece ${piece.name}:`, err)
}
return translation
}
const generateTranslationFile = async (pieceName: string) => {
const pieceRoot = await findPiece(pieceName)
await buildPiece(pieceRoot)
const outputFolder = pieceRoot.replace('packages/', 'dist/packages/')
try{
await installDependencies(outputFolder)
const pieceFromModule = await findPieceInModule(outputFolder);
const i18n = generateTranslationFileFromPiece({actions: (pieceFromModule as any)._actions, triggers: (pieceFromModule as any)._triggers, description: (pieceFromModule as any).description, displayName: (pieceFromModule as any).displayName, auth: (pieceFromModule as any).auth});
const i18nFolder = join(pieceRoot, 'src', 'i18n')
await makeFolderRecursive(i18nFolder);
await writeFile(join(i18nFolder, 'translation.json'), JSON.stringify(i18n, null, 2));
console.log(chalk.yellow('✨'), `Translation file for piece created in ${i18nFolder}`);
} catch (error) {
console.error(chalk.red('❌'), `Error generating translation file for piece ${pieceName}, make sure you built the piece`,error);
}
};
export const generateTranslationFileForPieceCommand = new Command('generate-translation-file')
.description('Generate i18n for a piece')
.argument('<pieceName>', 'The name of the piece to generate i18n for')
.action(async (pieceName: string) => {
await generateTranslationFile(pieceName);
});
export const generateTranslationFileForAllPiecesCommand = new Command('generate-translation-file-for-all-pieces')
.description('Generate i18n for all pieces')
.requiredOption('--shard-index <shardIndex>', 'Zero-based shard index to process', (value) => parseInt(value, 10))
.requiredOption('--shard-total <shardTotal>', 'Total number of shards', (value) => parseInt(value, 10))
.action(async ({shardIndex, shardTotal}: { shardIndex: number; shardTotal: number }) => {
const piecesDirectory = join(process.cwd(), 'packages', 'pieces', 'community')
const pieces = (await findPieces(piecesDirectory)).map(piece => piece.split('/').pop());
let totalTime = 0
let indexAcrossAllPieces = 0
for (const piece of pieces) {
if ((indexAcrossAllPieces % shardTotal) !== shardIndex) {
indexAcrossAllPieces++
continue
}
const time= performance.now()
await generateTranslationFile(piece);
console.log(chalk.yellow('✨'), `Translation file for piece ${piece} created in ${(performance.now() - time)/1000}s`)
totalTime += (performance.now() - time)/1000
indexAcrossAllPieces++
}
console.log(chalk.yellow('✨'), `Total time taken to generate translation files for selected pieces: ${totalTime}s`)
});

View File

@@ -0,0 +1,50 @@
import { Command } from 'commander';
import chalk from 'chalk';
import { prompt } from 'inquirer';
import { nanoid } from 'nanoid';
import jwtLibrary from 'jsonwebtoken';
const KEY_ID = '1'
const ISSUER = 'activepieces'
const ALGORITHM = 'HS256'
export const generateWorkerTokenCommand = new Command('token')
.description('Generate a JWT token for worker authentication')
.action(async () => {
const answers = await prompt([
{
type: 'input',
name: 'jwtSecret',
message: 'Enter your JWT secret (should be the same as AP_JWT_SECRET used for the app server):',
validate: (input) => {
if (!input) {
return 'JWT secret is required';
}
return true;
}
}
]);
const payload = {
id: nanoid(),
type: 'WORKER',
};
// 100 years in seconds
const expiresIn = 100 * 365 * 24 * 60 * 60;
try {
const token = jwtLibrary.sign(payload, answers.jwtSecret, {
expiresIn,
keyid: KEY_ID,
algorithm: ALGORITHM,
issuer: ISSUER,
});
console.log(chalk.green('\nGenerated Worker Token, Please use it in AP_WORKER_TOKEN environment variable:'));
console.log(chalk.yellow(token));
} catch (error) {
console.error(chalk.red('Failed to generate token:'), error);
process.exit(1);
}
});

View File

@@ -0,0 +1,81 @@
import { Command } from "commander";
import { publishPieceFromFolder, findPiece, assertPieceExists } from '../utils/piece-utils';
import chalk from "chalk";
import inquirer from 'inquirer';
import * as dotenv from 'dotenv';
dotenv.config({path: 'packages/server/api/.env'});
async function publishPiece(
{apiUrl, apiKey, pieceName, failOnError}:
{apiUrl: string,
apiKey: string,
pieceName: string,
failOnError: boolean,}
) {
const pieceFolder = await findPiece(pieceName);
assertPieceExists(pieceFolder)
await publishPieceFromFolder({
pieceFolder,
apiUrl,
apiKey,
failOnError
});
}
function assertNullOrUndefinedOrEmpty(value: any, message: string) {
if (value === undefined || value === null || (typeof value === 'string' && value.trim() === '')) {
console.error(chalk.red(message));
process.exit(1);
}
}
export const publishPieceCommand = new Command('publish')
.description('Publish pieces to the platform')
.option('-f, --fail-on-error', 'Exit the process if an error occurs while syncing a piece', false)
.action(async (command) => {
const questions = [
{
type: 'input',
name: 'name',
message: 'Enter the piece folder name',
placeholder: 'google-drive',
},
{
type: 'input',
name: 'apiUrl',
message: 'Enter the API URL',
placeholder: 'https://cloud.activepieces.com/api',
},
{
type: 'list',
name: 'apiKeySource',
message: 'Select the API Key source',
choices: ['Env Variable (AP_API_KEY)', 'Manually'],
default: 'Env Variable (AP_API_KEY)'
}
]
const answers = await inquirer.prompt(questions);
if (answers.apiKeySource === 'Manually') {
const apiKeyAnswers = await inquirer.prompt([{
type: 'input',
name: 'apiKey',
message: 'Enter the API Key',
}]);
answers.apiKey = apiKeyAnswers.apiKey;
}
const apiKey = answers.apiKeySource === 'Env Variable (AP_API_KEY)' ? process.env.AP_API_KEY : answers.apiKey;
assertNullOrUndefinedOrEmpty(answers.name, 'Piece name is required');
assertNullOrUndefinedOrEmpty(answers.apiUrl, 'API URL is required');
assertNullOrUndefinedOrEmpty(apiKey, 'API Key is required');
const apiUrlWithoutTrailSlash = answers.apiUrl.replace(/\/$/, '');
const { failOnError } = command;
await publishPiece({
apiUrl: apiUrlWithoutTrailSlash,
apiKey,
pieceName: answers.name,
failOnError
});
});

View File

@@ -0,0 +1,43 @@
import { Command } from "commander";
import { findPieces, publishPieceFromFolder } from '../utils/piece-utils';
import chalk from "chalk";
import { join } from "path";
async function syncPieces(
params:
{apiUrl: string,
apiKey: string,
pieces: string[] | null,
failOnError: boolean,}
) {
const piecesDirectory = join(process.cwd(), 'packages', 'pieces', 'custom')
const pieceFolders = await findPieces(piecesDirectory, params.pieces);
for (const pieceFolder of pieceFolders) {
await publishPieceFromFolder({
pieceFolder,
...params
});
}
}
export const syncPieceCommand = new Command('sync')
.description('Find new pieces versions and sync them with the database')
.requiredOption('-h, --apiUrl <url>', 'API URL ex: https://cloud.activepieces.com/api')
.option('-p, --pieces <pieces...>', 'Specify one or more piece names to sync. ' +
'If not provided, all custom pieces in the directory will be synced.')
.option('-f, --fail-on-error', 'Exit the process if an error occurs while syncing a piece', false)
.action(async (options) => {
const apiKey = process.env.AP_API_KEY;
const pieces = options.pieces ? [...new Set<string>(options.pieces)] : null;
const failOnError = options.failOnError;
if (!apiKey) {
console.error(chalk.red('AP_API_KEY environment variable is required'));
process.exit(1);
}
await syncPieces({
apiUrl: options.apiUrl.replace(/\/$/, ''),
apiKey,
pieces,
failOnError
});
});

View File

@@ -0,0 +1,4 @@
import { exec as execCallback } from 'node:child_process';
import { promisify } from 'node:util';
export const exec = promisify(execCallback);

View File

@@ -0,0 +1,97 @@
import {
constants,
readFile,
access,
writeFile,
mkdir,
} from 'node:fs/promises';
export type PackageJson = {
name: string;
version: string;
keywords: string[];
};
export type ProjectJson = {
name: string;
targets?: {
prebuild?: {
executor: string;
dependsOn?: string[];
options: {
cwd: string;
command: string;
};
};
build?: {
dependsOn?: string[];
options?: {
buildableProjectDepsInPackageJsonType?:
| 'peerDependencies'
| 'dependencies';
updateBuildableProjectDepsInPackageJson: boolean;
assets?: ({
input: string;
output: string;
glob: string;
} | string)[];
};
};
lint: {
executor: string;
outputs: string[];
options?: {
lintFilePatterns: string[];
};
};
};
};
export const checkIfFileExists = async (filePath: string) => {
try {
await access(filePath, constants.F_OK);
return true;
} catch (e) {
return false;
}
};
const readJsonFile = async <T>(path: string): Promise<T> => {
const jsonFile = await readFile(path, { encoding: 'utf-8' });
return JSON.parse(jsonFile) as T;
};
const writeJsonFile = async (path: string, data: unknown): Promise<void> => {
const serializedData = JSON.stringify(data, null, 2);
await writeFile(path, serializedData, { encoding: 'utf-8' });
};
export const readPackageJson = async (path: string): Promise<PackageJson> => {
return await readJsonFile(`${path}/package.json`);
};
export const readProjectJson = async (path: string): Promise<ProjectJson> => {
return await readJsonFile(`${path}/project.json`);
};
export const readPackageEslint = async (path: string): Promise<any> => {
return await readJsonFile(`${path}/.eslintrc.json`);
};
export const writePackageEslint = async (
path: string,
eslint: any
): Promise<void> => {
return await writeJsonFile(`${path}/.eslintrc.json`, eslint);
};
export const writeProjectJson = async (
path: string,
projectJson: ProjectJson
): Promise<void> => {
return await writeJsonFile(`${path}/project.json`, projectJson);
};
export const makeFolderRecursive = async (path: string): Promise<void> => {
await mkdir(path, { recursive: true });
};

View File

@@ -0,0 +1,176 @@
import { readdir, stat } from 'node:fs/promises'
import * as path from 'path'
import { cwd } from 'node:process'
import { readPackageJson, readProjectJson } from './files'
import { exec } from './exec'
import axios from 'axios'
import chalk from 'chalk'
import FormData from 'form-data';
import fs from 'fs';
export const piecesPath = () => path.join(cwd(), 'packages', 'pieces')
export const customPiecePath = () => path.join(piecesPath(), 'custom')
/**
* Finds and returns the paths of specific pieces or all available pieces in a given directory.
*
* @param inputPath - The root directory to search for pieces. If not provided, a default path to custom pieces is used.
* @param pieces - An optional array of piece names to search for. If not provided, all pieces in the directory are returned.
* @returns A promise resolving to an array of strings representing the paths of the found pieces.
*/
export async function findPieces(inputPath?: string, pieces?: string[]): Promise<string[]> {
const piecesPath = inputPath ?? customPiecePath()
const piecesFolders = await traverseFolder(piecesPath)
if (pieces) {
return pieces.flatMap((piece) => {
const folder = piecesFolders.find((p) => {
const normalizedPath = path.normalize(p);
return normalizedPath.endsWith(path.sep + piece);
});
if (!folder) {
return [];
}
return [folder];
});
} else {
return piecesFolders
}
}
/**
* Finds and returns the path of a single piece. Exits the process if the piece is not found.
*
* @param pieceName - The name of the piece to search for.
* @returns A promise resolving to a string representing the path of the found piece. If not found, the process exits.
*/
export async function findPiece(pieceName: string): Promise<string | null> {
return (await findPieces(piecesPath(), [pieceName]))[0] ?? null;
}
export async function buildPiece(pieceFolder: string): Promise<{ outputFolder: string, outputFile: string }> {
const projectJson = await readProjectJson(pieceFolder);
await buildPackage(projectJson.name);
const compiledPath = `dist/packages/${removeStartingSlashes(pieceFolder).split(path.sep + 'packages')[1]}`;
const { stdout } = await exec('npm pack --json', { cwd: compiledPath });
const tarFileName = JSON.parse(stdout)[0].filename;
return {
outputFolder: compiledPath,
outputFile: path.join(compiledPath, tarFileName)
};
}
export async function buildPackage(projectName:string) {
await exec(`npx nx build ${projectName} --skip-cache`);
return {
outputFolder: `dist/packages/${projectName}`,
}
}
export async function publishPieceFromFolder(
{pieceFolder, apiUrl, apiKey, failOnError}:
{pieceFolder: string,
apiUrl: string,
apiKey: string,
failOnError: boolean,}
) {
const projectJson = await readProjectJson(pieceFolder);
const packageJson = await readPackageJson(pieceFolder);
await buildPackage(projectJson.name);
const { outputFile } = await buildPiece(pieceFolder);
const formData = new FormData();
console.log(chalk.blue(`Uploading ${outputFile}`));
formData.append('pieceArchive', fs.createReadStream(outputFile));
formData.append('pieceName', packageJson.name);
formData.append('pieceVersion', packageJson.version);
formData.append('packageType', 'ARCHIVE');
formData.append('scope', 'PLATFORM');
try {
await axios.post(`${apiUrl}/v1/pieces`, formData, {
headers: {
'Authorization': `Bearer ${apiKey}`,
...formData.getHeaders()
}
});
console.info(chalk.green(`Piece '${packageJson.name}' published.`));
} catch (error) {
if (axios.isAxiosError(error)) {
if (error.response?.status === 409) {
console.info(chalk.yellow(`Piece '${packageJson.name}' and '${packageJson.version}' already published.`));
} else if (error.response && Math.floor(error.response.status / 100) !== 2) {
console.info(chalk.red(`Error publishing piece '${packageJson.name}', ${error}` ));
if (failOnError) {
console.info(chalk.yellow(`Terminating process due to publish failure for piece '${packageJson.name}' (fail-on-error is enabled)`));
process.exit(1);
}
} else {
console.error(chalk.red(`Unexpected error: ${error.message}`));
if (failOnError) {
console.info(chalk.yellow(`Terminating process due to unexpected error for piece '${packageJson.name}' (fail-on-error is enabled)`));
process.exit(1);
}
}
} else {
console.error(chalk.red(`Unexpected error: ${error.message}`));
if (failOnError) {
console.info(chalk.yellow(`Terminating process due to unexpected error for piece '${packageJson.name}' (fail-on-error is enabled)`));
process.exit(1);
}
}
}
}
async function traverseFolder(folderPath: string): Promise<string[]> {
const paths: string[] = []
const directoryExists = await stat(folderPath).catch(() => null)
if (directoryExists && directoryExists.isDirectory()) {
const files = await readdir(folderPath)
for (const file of files) {
const filePath = path.join(folderPath, file)
const fileStats = await stat(filePath)
if (fileStats.isDirectory() && file !== 'node_modules' && file !== 'dist') {
paths.push(...await traverseFolder(filePath))
}
else if (file === 'package.json') {
paths.push(folderPath)
}
}
}
return paths
}
export function displayNameToKebabCase(displayName: string): string {
return displayName.toLowerCase().replace(/\s+/g, '-');
}
export function displayNameToCamelCase(input: string): string {
const words = input.split(' ');
const camelCaseWords = words.map((word, index) => {
if (index === 0) {
return word.toLowerCase();
} else {
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
}
});
return camelCaseWords.join('');
}
export const assertPieceExists = async (pieceName: string | null) => {
if (!pieceName) {
console.error(chalk.red(`🚨 Piece ${pieceName} not found`));
process.exit(1);
}
};
export const removeStartingSlashes = (str: string) => {
return str.startsWith('/') ? str.slice(1) : str;
}

View File

@@ -0,0 +1,16 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"module": "commonjs",
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.spec.json"
}
]
}

View File

@@ -0,0 +1,11 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "commonjs",
"outDir": "../../dist/out-tsc",
"declaration": true,
"types": ["node"]
},
"exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"],
"include": ["src/**/*.ts"]
}

View File

@@ -0,0 +1,14 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"module": "commonjs",
"types": ["jest", "node"]
},
"include": [
"jest.config.ts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.d.ts"
]
}

View File

@@ -0,0 +1,36 @@
The Activepieces Enterprise license (the “Enterprise License”)
Copyright (c) 2022-2023 Activepieces Inc.
With regard to the Activepieces Software:
This software and associated documentation files (the "Software") may only be
used in production, if you (and any entity that you represent) have agreed to,
and are in compliance with, the Activepieces Subscription Terms of Service, available
at https://activepieces.com/terms (the “Enterprise Terms”), or other
agreement governing the use of the Software, as agreed by you and Activepieces,
and otherwise have a valid Activepieces Enterprise license for the
correct number of user seats. Subject to the foregoing sentence, you are free to
modify this Software and publish patches to the Software. You agree that Activepieces
and/or its licensors (as applicable) retain all right, title and interest in and
to all such modifications and/or patches, and all such modifications and/or
patches may only be used, copied, modified, displayed, distributed, or otherwise
exploited with a valid Activepieces Enterprise license for the correct
number of user seats. Notwithstanding the foregoing, you may copy and modify
the Software for development and testing purposes, without requiring a
subscription. You agree that Activepieces and/or its licensors (as applicable) retain
all right, title and interest in and to all such modifications. You are not
granted any other rights beyond what is expressly stated herein. Subject to the
foregoing, it is forbidden to copy, merge, publish, distribute, sublicense,
and/or sell the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
For all third party components incorporated into the Activepieces Software, those
components are licensed under the original license provided by the owner of the
applicable component.

View File

@@ -0,0 +1,18 @@
{
"extends": ["../../server/api/.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {}
},
{
"files": ["*.ts", "*.tsx"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"rules": {}
}
]
}

View File

@@ -0,0 +1,7 @@
# ee-shared
This library was generated with [Nx](https://nx.dev).
## Building
Run `nx build ee-shared` to build the library.

View File

@@ -0,0 +1,5 @@
{
"name": "@activepieces/ee-shared",
"version": "0.0.11",
"type": "commonjs"
}

View File

@@ -0,0 +1,23 @@
{
"name": "ee-shared",
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "packages/ee/shared/src",
"projectType": "library",
"targets": {
"build": {
"executor": "@nx/js:tsc",
"outputs": ["{options.outputPath}"],
"options": {
"outputPath": "dist/packages/ee/shared",
"main": "packages/ee/shared/src/index.ts",
"tsConfig": "packages/ee/shared/tsconfig.lib.json",
"assets": ["packages/ee/shared/*.md"]
}
},
"lint": {
"executor": "@nx/eslint:lint",
"outputs": ["{options.outputFile}"]
}
},
"tags": []
}

View File

@@ -0,0 +1,18 @@
export * from './lib/billing'
export * from './lib/audit-events'
export * from './lib/git-repo'
export * from './lib/api-key'
export * from './lib/billing'
export * from './lib/project/project-requests'
export * from './lib/custom-domains'
export * from './lib/project-members/project-member-request'
export * from './lib/project-members/project-member'
export * from './lib/template'
export * from './lib/product-embed/app-credentials/index'
export * from './lib/product-embed/connection-keys/index'
export * from './lib/signing-key'
export * from './lib/managed-authn'
export * from './lib/oauth-apps'
export * from './lib/otp'
export * from './lib/authn'
export * from './lib/alerts'

View File

@@ -0,0 +1,16 @@
import { ApId, BaseModelSchema } from '@activepieces/shared'
import { Static, Type } from '@sinclair/typebox'
export enum AlertChannel {
EMAIL = 'EMAIL',
}
export const Alert = Type.Object({
...BaseModelSchema,
projectId: ApId,
channel: Type.Enum(AlertChannel),
receiver: Type.String({}),
})
export type Alert = Static<typeof Alert>

View File

@@ -0,0 +1,18 @@
import { ApId } from '@activepieces/shared'
import { Static, Type } from '@sinclair/typebox'
import { AlertChannel } from './alerts-dto'
export const ListAlertsParams = Type.Object({
projectId: ApId,
cursor: Type.Optional(Type.String()),
limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 100 })),
})
export type ListAlertsParams = Static<typeof ListAlertsParams>
export const CreateAlertParams = Type.Object({
projectId: ApId,
channel: Type.Enum(AlertChannel),
receiver: Type.String({}),
})
export type CreateAlertParams = Static<typeof CreateAlertParams>

View File

@@ -0,0 +1,2 @@
export * from './alerts-dto'
export * from './alerts-requests'

View File

@@ -0,0 +1,34 @@
import { ApId, BaseModelSchema } from '@activepieces/shared'
import { Static, Type } from '@sinclair/typebox'
export const ApiKey = Type.Object({
...BaseModelSchema,
platformId: ApId,
displayName: Type.String(),
hashedValue: Type.String(),
truncatedValue: Type.String(),
lastUsedAt: Type.Optional(Type.String()),
})
export type ApiKey = Static<typeof ApiKey>
export const ApiKeyResponseWithValue = Type.Composite([
Type.Omit(ApiKey, ['hashedValue']),
Type.Object({
value: Type.String(),
}),
])
export type ApiKeyResponseWithValue = Static<typeof ApiKeyResponseWithValue>
export const ApiKeyResponseWithoutValue = Type.Omit(ApiKey, ['hashedValue'])
export type ApiKeyResponseWithoutValue = Static<typeof ApiKeyResponseWithoutValue>
export const CreateApiKeyRequest = Type.Object({
displayName: Type.String(),
})
export type CreateApiKeyRequest = Static<typeof CreateApiKeyRequest>

View File

@@ -0,0 +1,384 @@
import {
AppConnectionWithoutSensitiveData,
BaseModelSchema,
Flow,
FlowOperationRequest,
FlowOperationType,
FlowRun,
FlowVersion,
Folder,
Project,
ProjectRelease,
ProjectRole,
User,
} from '@activepieces/shared'
import { Static, Type } from '@sinclair/typebox'
import { SigningKey } from '../signing-key'
export const ListAuditEventsRequest = Type.Object({
limit: Type.Optional(Type.Number()),
cursor: Type.Optional(Type.String()),
action: Type.Optional(Type.Array(Type.String())),
projectId: Type.Optional(Type.Array(Type.String())),
userId: Type.Optional(Type.String()),
createdBefore: Type.Optional(Type.String()),
createdAfter: Type.Optional(Type.String()),
})
export type ListAuditEventsRequest = Static<typeof ListAuditEventsRequest>
const UserMeta = Type.Pick(User, ['email', 'id', 'firstName', 'lastName'])
export enum ApplicationEventName {
FLOW_CREATED = 'flow.created',
FLOW_DELETED = 'flow.deleted',
FLOW_UPDATED = 'flow.updated',
FLOW_RUN_RESUMED = 'flow.run.resumed',
FLOW_RUN_STARTED = 'flow.run.started',
FLOW_RUN_FINISHED = 'flow.run.finished',
FOLDER_CREATED = 'folder.created',
FOLDER_UPDATED = 'folder.updated',
FOLDER_DELETED = 'folder.deleted',
CONNECTION_UPSERTED = 'connection.upserted',
CONNECTION_DELETED = 'connection.deleted',
USER_SIGNED_UP = 'user.signed.up',
USER_SIGNED_IN = 'user.signed.in',
USER_PASSWORD_RESET = 'user.password.reset',
USER_EMAIL_VERIFIED = 'user.email.verified',
SIGNING_KEY_CREATED = 'signing.key.created',
PROJECT_ROLE_CREATED = 'project.role.created',
PROJECT_ROLE_DELETED = 'project.role.deleted',
PROJECT_ROLE_UPDATED = 'project.role.updated',
PROJECT_RELEASE_CREATED = 'project.release.created',
}
const BaseAuditEventProps = {
...BaseModelSchema,
platformId: Type.String(),
projectId: Type.Optional(Type.String()),
projectDisplayName: Type.Optional(Type.String()),
userId: Type.Optional(Type.String()),
userEmail: Type.Optional(Type.String()),
ip: Type.Optional(Type.String()),
}
export const ConnectionEvent = Type.Object({
...BaseAuditEventProps,
action: Type.Union([
Type.Literal(ApplicationEventName.CONNECTION_DELETED),
Type.Literal(ApplicationEventName.CONNECTION_UPSERTED),
]),
data: Type.Object({
connection: Type.Pick(AppConnectionWithoutSensitiveData, [
'displayName',
'externalId',
'pieceName',
'status',
'type',
'id',
'created',
'updated',
]),
project: Type.Optional(Type.Pick(Project, ['displayName'])),
}),
})
export type ConnectionEvent = Static<typeof ConnectionEvent>
export const FolderEvent = Type.Object({
...BaseAuditEventProps,
action: Type.Union([
Type.Literal(ApplicationEventName.FOLDER_UPDATED),
Type.Literal(ApplicationEventName.FOLDER_CREATED),
Type.Literal(ApplicationEventName.FOLDER_DELETED),
]),
data: Type.Object({
folder: Type.Pick(Folder, ['id', 'displayName', 'created', 'updated']),
project: Type.Optional(Type.Pick(Project, ['displayName'])),
}),
})
export type FolderEvent = Static<typeof FolderEvent>
export const FlowRunEvent = Type.Object({
...BaseAuditEventProps,
action: Type.Union([
Type.Literal(ApplicationEventName.FLOW_RUN_STARTED),
Type.Literal(ApplicationEventName.FLOW_RUN_FINISHED),
Type.Literal(ApplicationEventName.FLOW_RUN_RESUMED),
]),
data: Type.Object({
flowRun: Type.Pick(FlowRun, [
'id',
'startTime',
'finishTime',
'duration',
'environment',
'flowId',
'flowVersionId',
'flowDisplayName',
'status',
]),
project: Type.Optional(Type.Pick(Project, ['displayName'])),
}),
})
export type FlowRunEvent = Static<typeof FlowRunEvent>
export const FlowCreatedEvent = Type.Object({
...BaseAuditEventProps,
action: Type.Literal(ApplicationEventName.FLOW_CREATED),
data: Type.Object({
flow: Type.Pick(Flow, ['id', 'created', 'updated']),
project: Type.Optional(Type.Pick(Project, ['displayName'])),
}),
})
export type FlowCreatedEvent = Static<typeof FlowCreatedEvent>
export const FlowDeletedEvent = Type.Object({
...BaseAuditEventProps,
action: Type.Literal(ApplicationEventName.FLOW_DELETED),
data: Type.Object({
flow: Type.Pick(Flow, ['id', 'created', 'updated']),
flowVersion: Type.Pick(FlowVersion, [
'id',
'displayName',
'flowId',
'created',
'updated',
]),
project: Type.Optional(Type.Pick(Project, ['displayName'])),
}),
})
export type FlowDeletedEvent = Static<typeof FlowDeletedEvent>
export const FlowUpdatedEvent = Type.Object({
...BaseAuditEventProps,
action: Type.Literal(ApplicationEventName.FLOW_UPDATED),
data: Type.Object({
flowVersion: Type.Pick(FlowVersion, [
'id',
'displayName',
'flowId',
'created',
'updated',
]),
request: FlowOperationRequest,
project: Type.Optional(Type.Pick(Project, ['displayName'])),
}),
})
export type FlowUpdatedEvent = Static<typeof FlowUpdatedEvent>
export const AuthenticationEvent = Type.Object({
...BaseAuditEventProps,
action: Type.Union([
Type.Literal(ApplicationEventName.USER_SIGNED_IN),
Type.Literal(ApplicationEventName.USER_PASSWORD_RESET),
Type.Literal(ApplicationEventName.USER_EMAIL_VERIFIED),
]),
data: Type.Object({
user: Type.Optional(UserMeta),
}),
})
export type AuthenticationEvent = Static<typeof AuthenticationEvent>
export const SignUpEvent = Type.Object({
...BaseAuditEventProps,
action: Type.Literal(ApplicationEventName.USER_SIGNED_UP),
data: Type.Object({
source: Type.Union([
Type.Literal('credentials'),
Type.Literal('sso'),
Type.Literal('managed'),
]),
user: Type.Optional(UserMeta),
}),
})
export type SignUpEvent = Static<typeof SignUpEvent>
export const SigningKeyEvent = Type.Object({
...BaseAuditEventProps,
action: Type.Union([Type.Literal(ApplicationEventName.SIGNING_KEY_CREATED)]),
data: Type.Object({
signingKey: Type.Pick(SigningKey, [
'id',
'created',
'updated',
'displayName',
]),
}),
})
export type SigningKeyEvent = Static<typeof SigningKeyEvent>
export const ProjectRoleEvent = Type.Object({
...BaseAuditEventProps,
action: Type.Union([
Type.Literal(ApplicationEventName.PROJECT_ROLE_CREATED),
Type.Literal(ApplicationEventName.PROJECT_ROLE_UPDATED),
Type.Literal(ApplicationEventName.PROJECT_ROLE_DELETED),
]),
data: Type.Object({
projectRole: Type.Pick(ProjectRole, [
'id',
'created',
'updated',
'name',
'permissions',
'platformId',
]),
}),
})
export type ProjectRoleEvent = Static<typeof ProjectRoleEvent>
export const ProjectReleaseEvent = Type.Object({
...BaseAuditEventProps,
action: Type.Literal(ApplicationEventName.PROJECT_RELEASE_CREATED),
data: Type.Object({
release: Type.Pick(ProjectRelease, ['name', 'description', 'type', 'projectId', 'importedByUser']),
}),
})
export type ProjectReleaseEvent = Static<typeof ProjectReleaseEvent>
export const ApplicationEvent = Type.Union([
ConnectionEvent,
FlowCreatedEvent,
FlowDeletedEvent,
FlowUpdatedEvent,
FlowRunEvent,
AuthenticationEvent,
FolderEvent,
SignUpEvent,
SigningKeyEvent,
ProjectRoleEvent,
ProjectReleaseEvent,
])
export type ApplicationEvent = Static<typeof ApplicationEvent>
export function summarizeApplicationEvent(event: ApplicationEvent) {
switch (event.action) {
case ApplicationEventName.FLOW_UPDATED: {
return convertUpdateActionToDetails(event)
}
case ApplicationEventName.FLOW_RUN_STARTED:
return `Flow run ${event.data.flowRun.id} is started`
case ApplicationEventName.FLOW_RUN_FINISHED: {
return `Flow run ${event.data.flowRun.id} is finished`
}
case ApplicationEventName.FLOW_RUN_RESUMED: {
return `Flow run ${event.data.flowRun.id} is resumed`
}
case ApplicationEventName.FLOW_CREATED:
return `Flow ${event.data.flow.id} is created`
case ApplicationEventName.FLOW_DELETED:
return `Flow ${event.data.flow.id} (${event.data.flowVersion.displayName}) is deleted`
case ApplicationEventName.FOLDER_CREATED:
return `${event.data.folder.displayName} is created`
case ApplicationEventName.FOLDER_UPDATED:
return `${event.data.folder.displayName} is updated`
case ApplicationEventName.FOLDER_DELETED:
return `${event.data.folder.displayName} is deleted`
case ApplicationEventName.CONNECTION_UPSERTED:
return `${event.data.connection.displayName} (${event.data.connection.externalId}) is updated`
case ApplicationEventName.CONNECTION_DELETED:
return `${event.data.connection.displayName} (${event.data.connection.externalId}) is deleted`
case ApplicationEventName.USER_SIGNED_IN:
return `User ${event.userEmail} signed in`
case ApplicationEventName.USER_PASSWORD_RESET:
return `User ${event.userEmail} reset password`
case ApplicationEventName.USER_EMAIL_VERIFIED:
return `User ${event.userEmail} verified email`
case ApplicationEventName.USER_SIGNED_UP:
return `User ${event.userEmail} signed up using email from ${event.data.source}`
case ApplicationEventName.SIGNING_KEY_CREATED:
return `${event.data.signingKey.displayName} is created`
case ApplicationEventName.PROJECT_ROLE_CREATED:
return `${event.data.projectRole.name} is created`
case ApplicationEventName.PROJECT_ROLE_UPDATED:
return `${event.data.projectRole.name} is updated`
case ApplicationEventName.PROJECT_ROLE_DELETED:
return `${event.data.projectRole.name} is deleted`
case ApplicationEventName.PROJECT_RELEASE_CREATED:
return `${event.data.release.name} is created`
}
}
function convertUpdateActionToDetails(event: FlowUpdatedEvent) {
switch (event.data.request.type) {
case FlowOperationType.ADD_ACTION:
return `Added action "${event.data.request.request.action.displayName}" to "${event.data.flowVersion.displayName}" Flow.`
case FlowOperationType.UPDATE_ACTION:
return `Updated action "${event.data.request.request.displayName}" in "${event.data.flowVersion.displayName}" Flow.`
case FlowOperationType.DELETE_ACTION:
{
const request = event.data.request.request
const names = request.names
return `Deleted actions "${names.join(', ')}" from "${event.data.flowVersion.displayName}" Flow.`
}
case FlowOperationType.CHANGE_NAME:
return `Renamed flow "${event.data.flowVersion.displayName}" to "${event.data.request.request.displayName}".`
case FlowOperationType.LOCK_AND_PUBLISH:
return `Locked and published flow "${event.data.flowVersion.displayName}" Flow.`
case FlowOperationType.USE_AS_DRAFT:
return `Unlocked and unpublished flow "${event.data.flowVersion.displayName}" Flow.`
case FlowOperationType.MOVE_ACTION:
return `Moved action "${event.data.request.request.name}" to after "${event.data.request.request.newParentStep}".`
case FlowOperationType.LOCK_FLOW:
return `Locked flow "${event.data.flowVersion.displayName}" Flow.`
case FlowOperationType.CHANGE_STATUS:
return `Changed status of flow "${event.data.flowVersion.displayName}" Flow to "${event.data.request.request.status}".`
case FlowOperationType.DUPLICATE_ACTION:
return `Duplicated action "${event.data.request.request.stepName}" in "${event.data.flowVersion.displayName}" Flow.`
case FlowOperationType.IMPORT_FLOW:
return `Imported flow in "${event.data.request.request.displayName}" Flow.`
case FlowOperationType.UPDATE_TRIGGER:
return `Updated trigger in "${event.data.flowVersion.displayName}" Flow to "${event.data.request.request.displayName}".`
case FlowOperationType.CHANGE_FOLDER:
return `Moved flow "${event.data.flowVersion.displayName}" to folder id ${event.data.request.request.folderId}.`
case FlowOperationType.DELETE_BRANCH: {
return `Deleted branch number ${
event.data.request.request.branchIndex + 1
} in flow "${event.data.flowVersion.displayName}" for the step "${
event.data.request.request.stepName
}".`
}
case FlowOperationType.SAVE_SAMPLE_DATA: {
return `Saved sample data for step "${event.data.request.request.stepName}" in flow "${event.data.flowVersion.displayName}".`
}
case FlowOperationType.DUPLICATE_BRANCH: {
return `Duplicated branch number ${
event.data.request.request.branchIndex + 1
} in flow "${event.data.flowVersion.displayName}" for the step "${
event.data.request.request.stepName
}".`
}
case FlowOperationType.ADD_BRANCH:
return `Added branch number ${
event.data.request.request.branchIndex + 1
} in flow "${event.data.flowVersion.displayName}" for the step "${
event.data.request.request.stepName
}".`
case FlowOperationType.SET_SKIP_ACTION:
{
const request = event.data.request.request
const names = request.names
return `Updated actions "${names.join(', ')}" in "${event.data.flowVersion.displayName}" Flow to skip.`
}
case FlowOperationType.UPDATE_METADATA:
return `Updated metadata for flow "${event.data.flowVersion.displayName}".`
case FlowOperationType.UPDATE_MINUTES_SAVED:
return `Updated minutes saved for flow "${event.data.flowVersion.displayName}".`
case FlowOperationType.MOVE_BRANCH:
return `Moved branch number ${
event.data.request.request.sourceBranchIndex + 1
} to ${
event.data.request.request.targetBranchIndex + 1
} in flow "${event.data.flowVersion.displayName}" for the step "${
event.data.request.request.stepName
}".`
}
}

View File

@@ -0,0 +1,82 @@
import { DefaultProjectRole, Permission } from '@activepieces/shared'
export const rolePermissions: Record<DefaultProjectRole, Permission[]> = {
[DefaultProjectRole.ADMIN]: [
Permission.READ_APP_CONNECTION,
Permission.WRITE_APP_CONNECTION,
Permission.READ_FLOW,
Permission.WRITE_FLOW,
Permission.UPDATE_FLOW_STATUS,
Permission.READ_PROJECT_MEMBER,
Permission.WRITE_PROJECT_MEMBER,
Permission.WRITE_INVITATION,
Permission.READ_INVITATION,
Permission.WRITE_PROJECT_RELEASE,
Permission.READ_PROJECT_RELEASE,
Permission.READ_RUN,
Permission.WRITE_RUN,
Permission.WRITE_ALERT,
Permission.READ_ALERT,
Permission.WRITE_PROJECT,
Permission.READ_PROJECT,
Permission.WRITE_FOLDER,
Permission.READ_FOLDER,
Permission.READ_TODOS,
Permission.WRITE_TODOS,
Permission.READ_TABLE,
Permission.WRITE_TABLE,
Permission.READ_MCP,
Permission.WRITE_MCP,
],
[DefaultProjectRole.EDITOR]: [
Permission.READ_APP_CONNECTION,
Permission.WRITE_APP_CONNECTION,
Permission.READ_FLOW,
Permission.WRITE_FLOW,
Permission.UPDATE_FLOW_STATUS,
Permission.READ_PROJECT_MEMBER,
Permission.READ_INVITATION,
Permission.WRITE_PROJECT_RELEASE,
Permission.READ_PROJECT_RELEASE,
Permission.READ_RUN,
Permission.WRITE_RUN,
Permission.READ_PROJECT,
Permission.WRITE_FOLDER,
Permission.READ_FOLDER,
Permission.READ_TODOS,
Permission.WRITE_TODOS,
Permission.READ_TABLE,
Permission.WRITE_TABLE,
Permission.READ_MCP,
Permission.WRITE_MCP,
],
[DefaultProjectRole.OPERATOR]: [
Permission.READ_APP_CONNECTION,
Permission.WRITE_APP_CONNECTION,
Permission.READ_FLOW,
Permission.UPDATE_FLOW_STATUS,
Permission.READ_PROJECT_MEMBER,
Permission.READ_INVITATION,
Permission.READ_PROJECT_RELEASE,
Permission.READ_RUN,
Permission.WRITE_RUN,
Permission.READ_PROJECT,
Permission.READ_FOLDER,
Permission.READ_TODOS,
Permission.WRITE_TODOS,
Permission.READ_TABLE,
Permission.READ_MCP,
],
[DefaultProjectRole.VIEWER]: [
Permission.READ_APP_CONNECTION,
Permission.READ_FLOW,
Permission.READ_PROJECT_MEMBER,
Permission.READ_INVITATION,
Permission.READ_PROJECT,
Permission.READ_RUN,
Permission.READ_FOLDER,
Permission.READ_TODOS,
Permission.READ_TABLE,
Permission.READ_MCP,
],
}

View File

@@ -0,0 +1 @@
export * from './requests'

View File

@@ -0,0 +1,25 @@
import { ApId, SignUpRequest } from '@activepieces/shared'
import { Static, Type } from '@sinclair/typebox'
export const VerifyEmailRequestBody = Type.Object({
identityId: ApId,
otp: Type.String(),
})
export type VerifyEmailRequestBody = Static<typeof VerifyEmailRequestBody>
export const ResetPasswordRequestBody = Type.Object({
identityId: ApId,
otp: Type.String(),
newPassword: Type.String(),
})
export type ResetPasswordRequestBody = Static<typeof ResetPasswordRequestBody>
export const SignUpAndAcceptRequestBody = Type.Composite([
Type.Omit(SignUpRequest, ['referringUserId', 'email']),
Type.Object({
invitationToken: Type.String(),
}),
])
export type SignUpAndAcceptRequestBody = Static<typeof SignUpAndAcceptRequestBody>

View File

@@ -0,0 +1,2 @@
export * from './enterprise-local-authn'
export * from './access-control-list'

View File

@@ -0,0 +1,135 @@
import { AiOverageState, isNil, PiecesFilterType, PlanName, PlatformPlanWithOnlyLimits, PlatformUsageMetric, TeamProjectsLimit } from '@activepieces/shared'
import { Static, Type } from '@sinclair/typebox'
export const PRICE_PER_EXTRA_ACTIVE_FLOWS = 5
export const AI_CREDITS_USAGE_THRESHOLD = 15000
export type ProjectPlanLimits = {
nickname?: string
locked?: boolean
pieces?: string[]
aiCredits?: number | null
piecesFilterType?: PiecesFilterType
}
export enum ApSubscriptionStatus {
ACTIVE = 'active',
CANCELED = 'canceled',
}
export const METRIC_TO_LIMIT_MAPPING = {
[PlatformUsageMetric.ACTIVE_FLOWS]: 'activeFlowsLimit',
} as const
export const METRIC_TO_USAGE_MAPPING = {
[PlatformUsageMetric.ACTIVE_FLOWS]: 'activeFlows',
} as const
export const SetAiCreditsOverageLimitParamsSchema = Type.Object({
limit: Type.Number({ minimum: 10 }),
})
export type SetAiCreditsOverageLimitParams = Static<typeof SetAiCreditsOverageLimitParamsSchema>
export const ToggleAiCreditsOverageEnabledParamsSchema = Type.Object({
state: Type.Enum(AiOverageState),
})
export type ToggleAiCreditsOverageEnabledParams = Static<typeof ToggleAiCreditsOverageEnabledParamsSchema>
export const UpdateActiveFlowsAddonParamsSchema = Type.Object({
newActiveFlowsLimit: Type.Number(),
})
export type UpdateActiveFlowsAddonParams = Static<typeof UpdateActiveFlowsAddonParamsSchema>
export const CreateCheckoutSessionParamsSchema = Type.Object({
newActiveFlowsLimit: Type.Number(),
})
export type CreateSubscriptionParams = Static<typeof CreateCheckoutSessionParamsSchema>
export enum PRICE_NAMES {
AI_CREDITS = 'ai-credit',
ACTIVE_FLOWS = 'active-flow',
}
export const PRICE_ID_MAP = {
[PRICE_NAMES.AI_CREDITS]: {
dev: 'price_1RnbNPQN93Aoq4f8GLiZbJFj',
prod: 'price_1Rnj5bKZ0dZRqLEKQx2gwL7s',
},
[PRICE_NAMES.ACTIVE_FLOWS]: {
dev: 'price_1SQbbYQN93Aoq4f8WK2JC4sf',
prod: 'price_1SQbcvKZ0dZRqLEKHV5UepRx',
},
}
export const STANDARD_CLOUD_PLAN: PlatformPlanWithOnlyLimits = {
plan: 'standard',
includedAiCredits: 200,
aiCreditsOverageLimit: undefined,
aiCreditsOverageState: AiOverageState.ALLOWED_BUT_OFF,
activeFlowsLimit: 10,
projectsLimit: 1,
agentsEnabled: true,
tablesEnabled: true,
todosEnabled: true,
mcpsEnabled: true,
embeddingEnabled: false,
globalConnectionsEnabled: false,
customRolesEnabled: false,
environmentsEnabled: false,
analyticsEnabled: true,
showPoweredBy: false,
auditLogEnabled: false,
managePiecesEnabled: false,
manageTemplatesEnabled: false,
customAppearanceEnabled: false,
teamProjectsLimit: TeamProjectsLimit.ONE,
projectRolesEnabled: false,
customDomainsEnabled: false,
apiKeysEnabled: false,
ssoEnabled: false,
}
export const OPEN_SOURCE_PLAN: PlatformPlanWithOnlyLimits = {
embeddingEnabled: false,
globalConnectionsEnabled: false,
customRolesEnabled: false,
mcpsEnabled: true,
tablesEnabled: true,
todosEnabled: true,
agentsEnabled: true,
includedAiCredits: 0,
aiCreditsOverageLimit: undefined,
aiCreditsOverageState: AiOverageState.NOT_ALLOWED,
environmentsEnabled: false,
analyticsEnabled: true,
showPoweredBy: false,
auditLogEnabled: false,
managePiecesEnabled: false,
manageTemplatesEnabled: false,
customAppearanceEnabled: false,
teamProjectsLimit: TeamProjectsLimit.NONE,
projectRolesEnabled: false,
customDomainsEnabled: false,
apiKeysEnabled: false,
ssoEnabled: false,
stripeCustomerId: undefined,
stripeSubscriptionId: undefined,
stripeSubscriptionStatus: undefined,
}
export const APPSUMO_PLAN = (planName: PlanName): PlatformPlanWithOnlyLimits => ({
...STANDARD_CLOUD_PLAN,
plan: planName,
activeFlowsLimit: undefined,
})
export const isCloudPlanButNotEnterprise = (plan?: string): boolean => {
if (isNil(plan)) {
return false
}
return plan === PlanName.STANDARD
}

View File

@@ -0,0 +1,32 @@
import { BaseModelSchema } from '@activepieces/shared'
import { Static, Type } from '@sinclair/typebox'
export enum CustomDomainStatus {
ACTIVE = 'ACTIVE',
PENDING = 'PENDING',
}
export const CustomDomain = Type.Object({
...BaseModelSchema,
domain: Type.String(),
platformId: Type.String(),
status: Type.Enum(CustomDomainStatus),
})
export type CustomDomain = Static<typeof CustomDomain>
export const AddDomainRequest = Type.Object({
domain: Type.String({
pattern: '^(?!.*\\.example\\.com$)(?!.*\\.example\\.net$).*',
}),
})
export type AddDomainRequest = Static<typeof AddDomainRequest>
export const ListCustomDomainsRequest = Type.Object({
limit: Type.Optional(Type.Number()),
cursor: Type.Optional(Type.String()),
})
export type ListCustomDomainsRequest = Static<typeof ListCustomDomainsRequest>

View File

@@ -0,0 +1,83 @@
import { BaseModelSchema } from '@activepieces/shared'
import { Static, Type } from '@sinclair/typebox'
export enum GitBranchType {
PRODUCTION = 'PRODUCTION',
DEVELOPMENT = 'DEVELOPMENT',
}
export const GitRepo = Type.Object({
...BaseModelSchema,
remoteUrl: Type.String(),
branch: Type.String(),
branchType: Type.Enum(GitBranchType),
projectId: Type.String(),
sshPrivateKey: Type.String(),
slug: Type.String(),
})
export type GitRepo = Static<typeof GitRepo>
export const GitRepoWithoutSensitiveData = Type.Omit(GitRepo, ['sshPrivateKey'])
export type GitRepoWithoutSensitiveData = Static<typeof GitRepoWithoutSensitiveData>
export enum GitPushOperationType {
PUSH_FLOW = 'PUSH_FLOW',
DELETE_FLOW = 'DELETE_FLOW',
PUSH_TABLE = 'PUSH_TABLE',
DELETE_TABLE = 'DELETE_TABLE',
PUSH_EVERYTHING = 'PUSH_EVERYTHING',
}
export const PushFlowsGitRepoRequest = Type.Object({
type: Type.Union([Type.Literal(GitPushOperationType.PUSH_FLOW), Type.Literal(GitPushOperationType.DELETE_FLOW)]),
commitMessage: Type.String({
minLength: 1,
}),
externalFlowIds: Type.Array(Type.String()),
})
export type PushFlowsGitRepoRequest = Static<typeof PushFlowsGitRepoRequest>
export const PushTablesGitRepoRequest = Type.Object({
type: Type.Union([Type.Literal(GitPushOperationType.PUSH_TABLE), Type.Literal(GitPushOperationType.DELETE_TABLE)]),
commitMessage: Type.String({
minLength: 1,
}),
externalTableIds: Type.Array(Type.String()),
})
export type PushTablesGitRepoRequest = Static<typeof PushTablesGitRepoRequest>
export const PushEverythingGitRepoRequest = Type.Object({
type: Type.Literal(GitPushOperationType.PUSH_EVERYTHING),
commitMessage: Type.String({
minLength: 1,
}),
})
export type PushEverythingGitRepoRequest = Static<typeof PushEverythingGitRepoRequest>
export const PushGitRepoRequest = Type.Union([PushFlowsGitRepoRequest, PushTablesGitRepoRequest, PushEverythingGitRepoRequest])
export type PushGitRepoRequest = Static<typeof PushGitRepoRequest>
export const ConfigureRepoRequest = Type.Object({
projectId: Type.String({
minLength: 1,
}),
remoteUrl: Type.String({
pattern: '^git@',
}),
branch: Type.String({
minLength: 1,
}),
branchType: Type.Enum(GitBranchType),
sshPrivateKey: Type.String({
minLength: 1,
}),
slug: Type.String({
minLength: 1,
}),
})
export type ConfigureRepoRequest = Static<typeof ConfigureRepoRequest>

View File

@@ -0,0 +1 @@
export * from './managed-authn-requests'

View File

@@ -0,0 +1,8 @@
import { Static, Type } from '@sinclair/typebox'
export const ManagedAuthnRequestBody = Type.Object({
//if you change this you need to update the embed-sdk I can't import it there because it can't have dependencies
externalAccessToken: Type.String(),
})
export type ManagedAuthnRequestBody = Static<typeof ManagedAuthnRequestBody>

View File

@@ -0,0 +1 @@
export * from './oauth-app'

View File

@@ -0,0 +1,26 @@
import { BaseModelSchema } from '@activepieces/shared'
import { Static, Type } from '@sinclair/typebox'
export const OAuthApp = Type.Object({
...BaseModelSchema,
pieceName: Type.String(),
platformId: Type.String(),
clientId: Type.String(),
})
export type OAuthApp = Static<typeof OAuthApp>
export const UpsertOAuth2AppRequest = Type.Object({
pieceName: Type.String(),
clientId: Type.String(),
clientSecret: Type.String(),
})
export type UpsertOAuth2AppRequest = Static<typeof UpsertOAuth2AppRequest>
export const ListOAuth2AppRequest = Type.Object({
limit: Type.Optional(Type.Number()),
cursor: Type.Optional(Type.String()),
})
export type ListOAuth2AppRequest = Static<typeof ListOAuth2AppRequest>

View File

@@ -0,0 +1,3 @@
export * from './otp-model'
export * from './otp-requests'
export * from './otp-type'

View File

@@ -0,0 +1,20 @@
import { ApId, BaseModelSchema } from '@activepieces/shared'
import { Static, Type } from '@sinclair/typebox'
import { OtpType } from './otp-type'
export type OtpId = ApId
export enum OtpState {
PENDING = 'PENDING',
CONFIRMED = 'CONFIRMED',
}
export const OtpModel = Type.Object({
...BaseModelSchema,
type: Type.Enum(OtpType),
identityId: ApId,
value: Type.String(),
state: Type.Enum(OtpState),
})
export type OtpModel = Static<typeof OtpModel>

View File

@@ -0,0 +1,11 @@
import { Static, Type } from '@sinclair/typebox'
import { OtpType } from './otp-type'
export const CreateOtpRequestBody = Type.Object({
email: Type.String(),
type: Type.Enum(OtpType),
})
export type CreateOtpRequestBody = Static<typeof CreateOtpRequestBody>

View File

@@ -0,0 +1,4 @@
export enum OtpType {
EMAIL_VERIFICATION = 'EMAIL_VERIFICATION',
PASSWORD_RESET = 'PASSWORD_RESET',
}

View File

@@ -0,0 +1,38 @@
import { Static, Type } from '@sinclair/typebox'
import { AppCredentialType } from './app-credentials'
export const ListAppCredentialsRequest = Type.Object({
projectId: Type.String(),
appName: Type.Optional(Type.String()),
limit: Type.Optional(Type.Number()),
cursor: Type.Optional(Type.String({})),
})
export type ListAppCredentialsRequest = Static<typeof ListAppCredentialsRequest>
export const UpsertApiKeyCredentialRequest = Type.Object({
id: Type.Optional(Type.String()),
appName: Type.String(),
settings: Type.Object({
type: Type.Literal(AppCredentialType.API_KEY),
}),
})
export const UpsertOAuth2CredentialRequest = Type.Object({
id: Type.Optional(Type.String()),
appName: Type.String(),
settings: Type.Object({
type: Type.Literal(AppCredentialType.OAUTH2),
authUrl: Type.String({}),
scope: Type.String(),
tokenUrl: Type.String({}),
clientId: Type.String({}),
clientSecret: Type.String({}),
}),
})
export const UpsertAppCredentialRequest = Type.Union([UpsertOAuth2CredentialRequest, UpsertApiKeyCredentialRequest])
export type UpsertAppCredentialRequest = Static<typeof UpsertAppCredentialRequest>

View File

@@ -0,0 +1,29 @@
import { BaseModel, OAuth2GrantType, ProjectId } from '@activepieces/shared'
export type AppCredentialId = string
export type AppOAuth2Settings = {
type: AppCredentialType.OAUTH2
authUrl: string
tokenUrl: string
grantType: OAuth2GrantType
clientId: string
clientSecret?: string
scope: string
}
export type AppApiKeySettings = {
type: AppCredentialType.API_KEY
}
export type AppCredential = {
appName: string
projectId: ProjectId
settings: AppOAuth2Settings | AppApiKeySettings
} & BaseModel<AppCredentialId>
export enum AppCredentialType {
OAUTH2 = 'OAUTH2',
API_KEY = 'API_KEY',
}

View File

@@ -0,0 +1,2 @@
export * from './app-credentials'
export * from './app-credentials-requests'

View File

@@ -0,0 +1,18 @@
import { BaseModel, ProjectId } from '@activepieces/shared'
export type ConnectionKeyId = string
export type ConnectionKey = {
projectId: ProjectId
settings: SigningKeyConnection
} & BaseModel<ConnectionKeyId>
export type SigningKeyConnection = {
type: ConnectionKeyType.SIGNING_KEY
publicKey: string
privateKey?: string
}
export enum ConnectionKeyType {
SIGNING_KEY = 'SIGNING_KEY',
}

View File

@@ -0,0 +1,49 @@
import { Static, Type } from '@sinclair/typebox'
import { ConnectionKeyType } from './connection-key'
export const GetOrDeleteConnectionFromTokenRequest = Type.Object({
projectId: Type.String(),
token: Type.String(),
appName: Type.String(),
})
export type GetOrDeleteConnectionFromTokenRequest = Static<typeof GetOrDeleteConnectionFromTokenRequest>
export const ListConnectionKeysRequest = Type.Object({
limit: Type.Optional(Type.Number()),
cursor: Type.Optional(Type.String({})),
})
export type ListConnectionKeysRequest = Static<typeof ListConnectionKeysRequest>
export const UpsertApiKeyConnectionFromToken = Type.Object({
appCredentialId: Type.String(),
apiKey: Type.String(),
token: Type.String(),
})
export type UpsertApiKeyConnectionFromToken = Static<typeof UpsertApiKeyConnectionFromToken>
export const UpsertOAuth2ConnectionFromToken = Type.Object({
appCredentialId: Type.String(),
props: Type.Record(Type.String(), Type.Any()),
token: Type.String(),
code: Type.String(),
redirectUrl: Type.String(),
})
export type UpsertOAuth2ConnectionFromToken = Static<typeof UpsertOAuth2ConnectionFromToken>
export const UpsertConnectionFromToken = Type.Union([UpsertApiKeyConnectionFromToken, UpsertOAuth2ConnectionFromToken])
export type UpsertConnectionFromToken = Static<typeof UpsertConnectionFromToken>
export const UpsertSigningKeyConnection = Type.Object({
settings: Type.Object({
type: Type.Literal(ConnectionKeyType.SIGNING_KEY),
}),
})
export type UpsertSigningKeyConnection = Static<typeof UpsertSigningKeyConnection>

View File

@@ -0,0 +1,2 @@
export * from './connection-key'
export * from './connection-requests'

View File

@@ -0,0 +1,28 @@
import { Static, Type } from '@sinclair/typebox'
export const AcceptInvitationRequest = Type.Object({
token: Type.String(),
})
export type AcceptInvitationRequest = Static<typeof AcceptInvitationRequest>
export const ListProjectMembersRequestQuery = Type.Object({
projectId: Type.String(),
projectRoleId: Type.Optional(Type.String()),
cursor: Type.Optional(Type.String()),
limit: Type.Optional(Type.Number()),
})
export type ListProjectMembersRequestQuery = Static<typeof ListProjectMembersRequestQuery>
export const AcceptProjectResponse = Type.Object({
registered: Type.Boolean(),
})
export type AcceptProjectResponse = Static<typeof AcceptProjectResponse>
export const UpdateProjectMemberRoleRequestBody = Type.Object({
role: Type.String(),
})
export type UpdateProjectMemberRoleRequestBody = Static<typeof UpdateProjectMemberRoleRequestBody>

View File

@@ -0,0 +1,24 @@
import { ApId, BaseModelSchema, ProjectMetaData, ProjectRole, UserWithMetaInformation } from '@activepieces/shared'
import { Static, Type } from '@sinclair/typebox'
export type ProjectMemberId = string
export const ProjectMember = Type.Object({
...BaseModelSchema,
platformId: ApId,
userId: ApId,
projectId: Type.String(),
projectRoleId: ApId,
}, {
description: 'Project member is which user is assigned to a project.',
})
export type ProjectMember = Static<typeof ProjectMember>
export const ProjectMemberWithUser = Type.Composite([ProjectMember, Type.Object({
user: UserWithMetaInformation,
projectRole: ProjectRole,
project: ProjectMetaData,
})])
export type ProjectMemberWithUser = Static<typeof ProjectMemberWithUser>

View File

@@ -0,0 +1,39 @@
import { Metadata, Nullable, PiecesFilterType, ProjectIcon, ProjectType, SAFE_STRING_PATTERN } from '@activepieces/shared'
import { Static, Type } from '@sinclair/typebox'
export const UpdateProjectPlatformRequest = Type.Object({
releasesEnabled: Type.Optional(Type.Boolean()),
displayName: Type.Optional(Type.String({
pattern: SAFE_STRING_PATTERN,
})),
externalId: Type.Optional(Type.String()),
metadata: Type.Optional(Metadata),
icon: Type.Optional(ProjectIcon),
plan: Type.Optional(Type.Object({
pieces: Type.Optional(Type.Array(Type.String({}))),
piecesFilterType: Type.Optional(Type.Enum(PiecesFilterType)),
})),
})
export type UpdateProjectPlatformRequest = Static<typeof UpdateProjectPlatformRequest>
export const CreatePlatformProjectRequest = Type.Object({
displayName: Type.String({
pattern: SAFE_STRING_PATTERN,
}),
externalId: Nullable(Type.String()),
metadata: Nullable(Metadata),
maxConcurrentJobs: Nullable(Type.Number()),
})
export type CreatePlatformProjectRequest = Static<typeof CreatePlatformProjectRequest>
export const ListProjectRequestForPlatformQueryParams = Type.Object({
externalId: Type.Optional(Type.String()),
limit: Type.Optional(Type.Number({})),
cursor: Type.Optional(Type.String({})),
displayName: Type.Optional(Type.String()),
types: Type.Optional(Type.Array(Type.Enum(ProjectType))),
})
export type ListProjectRequestForPlatformQueryParams = Static<typeof ListProjectRequestForPlatformQueryParams>

View File

@@ -0,0 +1,3 @@
export * from './signing-key-model'
export * from './signing-key-response'
export * from './signing-key.request'

View File

@@ -0,0 +1,19 @@
import { ApId, BaseModelSchema } from '@activepieces/shared'
import { Static, Type } from '@sinclair/typebox'
export enum KeyAlgorithm {
RSA = 'RSA',
}
export type SigningKeyId = ApId
export const SigningKey = Type.Object({
...BaseModelSchema,
platformId: ApId,
publicKey: Type.String(),
displayName: Type.String(),
/* algorithm used to generate this key pair */
algorithm: Type.Enum(KeyAlgorithm),
})
export type SigningKey = Static<typeof SigningKey>

View File

@@ -0,0 +1,5 @@
import { SigningKey } from './signing-key-model'
export type AddSigningKeyResponse = SigningKey & {
privateKey: string
}

View File

@@ -0,0 +1,7 @@
import { Static, Type } from '@sinclair/typebox'
export const AddSigningKeyRequestBody = Type.Object({
displayName: Type.String(),
})
export type AddSigningKeyRequestBody = Static<typeof AddSigningKeyRequestBody>

View File

@@ -0,0 +1,9 @@
import { Static, Type } from '@sinclair/typebox'
export const GetFlowTemplateRequestQuery = Type.Object({
versionId: Type.Optional(Type.String()),
})
export type GetFlowTemplateRequestQuery = Static<typeof GetFlowTemplateRequestQuery>

View File

@@ -0,0 +1 @@
export * from './flow-template.request'

View File

@@ -0,0 +1 @@
export * from './flow-template'

View File

@@ -0,0 +1,19 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"module": "commonjs",
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
}
]
}

View File

@@ -0,0 +1,10 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../../dist/out-tsc",
"declaration": true,
"types": ["node"]
},
"include": ["src/**/*.ts"],
"exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"]
}

View File

@@ -0,0 +1,17 @@
{
"extends": ["../../../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts"],
"rules": {
"@typescript-eslint/adjacent-overload-signatures": "off",
"@typescript-eslint/no-non-null-assertion": "off"
},
"extends": [
"plugin:prettier/recommended"
],
"plugins": ["prettier"]
}
]
}

View File

@@ -0,0 +1,18 @@
{
"extends": ["../../../../.eslintrc.base.json"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {}
},
{
"files": ["*.ts", "*.tsx"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"rules": {}
}
]
}

View File

@@ -0,0 +1,9 @@
# ee-embed-sdk
This library was generated with [Nx](https://nx.dev).
## Building
Run `nx bundle ee-embed-sdk` to build the library.
Check project.json "output" property to see the generated file location and its name.

View File

@@ -0,0 +1,7 @@
{
"name": "ee-embed-sdk",
"version": "0.8.1",
"type": "commonjs",
"main": "./src/index.js",
"typings": "./src/index.d.ts"
}

View File

@@ -0,0 +1,46 @@
{
"name": "ee-embed-sdk",
"$schema": "../../../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "packages/ee/ui/embed-sdk/src",
"projectType": "library",
"targets": {
"build": {
"executor": "@nx/js:tsc",
"outputs": ["{options.outputPath}"],
"options": {
"outputPath": "dist/packages/ee/ui/embed-sdk",
"main": "packages/ee/ui/embed-sdk/src/index.ts",
"tsConfig": "packages/ee/ui/embed-sdk/tsconfig.lib.json",
"assets": []
}
},
"bundle": {
"executor": "@nx/webpack:webpack",
"outputs": ["{options.outputPath}"],
"options": {
"target": "web",
"compiler": "tsc",
"outputFileName": "bundled.js",
"outputPath": "dist/packages/ee/ui/embed-sdk",
"main": "packages/ee/ui/embed-sdk/src/index.ts",
"tsConfig": "packages/ee/ui/embed-sdk/tsconfig.lib.json",
"assets": [],
"webpackConfig": "packages/ee/ui/embed-sdk/webpack.config.js",
"generatePackageJson": true,
"babelUpwardRootMode": true
},
"configurations": {
"production": {
"optimization": true,
"extractLicenses": true,
"inspect": false
}
}
},
"lint": {
"executor": "@nx/eslint:lint",
"outputs": ["{options.outputFile}"]
}
},
"tags": []
}

View File

@@ -0,0 +1,618 @@
//Client ==> Activepieces
//Vendor ==> Customers using our embed sdk
export enum ActivepiecesClientEventName {
CLIENT_INIT = 'CLIENT_INIT',
CLIENT_ROUTE_CHANGED = 'CLIENT_ROUTE_CHANGED',
CLIENT_NEW_CONNECTION_DIALOG_CLOSED = 'CLIENT_NEW_CONNECTION_DIALOG_CLOSED',
CLIENT_SHOW_CONNECTION_IFRAME = 'CLIENT_SHOW_CONNECTION_IFRAME',
CLIENT_CONNECTION_NAME_IS_INVALID = 'CLIENT_CONNECTION_NAME_IS_INVALID',
CLIENT_AUTHENTICATION_SUCCESS = 'CLIENT_AUTHENTICATION_SUCCESS',
CLIENT_AUTHENTICATION_FAILED = 'CLIENT_AUTHENTICATION_FAILED',
CLIENT_CONFIGURATION_FINISHED = 'CLIENT_CONFIGURATION_FINISHED',
CLIENT_CONNECTION_PIECE_NOT_FOUND = 'CLIENT_CONNECTION_PIECE_NOT_FOUND',
CLIENT_BUILDER_HOME_BUTTON_CLICKED = 'CLIENT_BUILDER_HOME_BUTTON_CLICKED',
}
export interface ActivepiecesClientInit {
type: ActivepiecesClientEventName.CLIENT_INIT;
data: Record<string, never>;
}
export interface ActivepiecesClientAuthenticationSuccess {
type: ActivepiecesClientEventName.CLIENT_AUTHENTICATION_SUCCESS;
data: Record<string, never>;
}
export interface ActivepiecesClientAuthenticationFailed {
type: ActivepiecesClientEventName.CLIENT_AUTHENTICATION_FAILED;
data: unknown;
}
// Added this event so in the future if we add another step between authentication and configuration finished, we can use this event to notify the parent
export interface ActivepiecesClientConfigurationFinished {
type: ActivepiecesClientEventName.CLIENT_CONFIGURATION_FINISHED;
data: Record<string, never>;
}
export interface ActivepiecesClientShowConnectionIframe {
type: ActivepiecesClientEventName.CLIENT_SHOW_CONNECTION_IFRAME;
data: Record<string, never>;
}
export interface ActivepiecesClientConnectionNameIsInvalid {
type: ActivepiecesClientEventName.CLIENT_CONNECTION_NAME_IS_INVALID;
data: {
error: string;
};
}
export interface ActivepiecesClientConnectionPieceNotFound {
type: ActivepiecesClientEventName.CLIENT_CONNECTION_PIECE_NOT_FOUND;
data: {
error: string
};
}
export interface ActivepiecesClientRouteChanged {
type: ActivepiecesClientEventName.CLIENT_ROUTE_CHANGED;
data: {
route: string;
};
}
export interface ActivepiecesNewConnectionDialogClosed {
type: ActivepiecesClientEventName.CLIENT_NEW_CONNECTION_DIALOG_CLOSED;
data: { connection?: { id: string; name: string } };
}
export interface ActivepiecesBuilderHomeButtonClicked {
type: ActivepiecesClientEventName.CLIENT_BUILDER_HOME_BUTTON_CLICKED;
data: {
route: string;
};
}
type IframeWithWindow = HTMLIFrameElement & { contentWindow: Window };
export const NEW_CONNECTION_QUERY_PARAMS = {
name: 'pieceName',
connectionName: 'connectionName',
randomId: 'randomId'
};
export type ActivepiecesClientEvent =
| ActivepiecesClientInit
| ActivepiecesClientRouteChanged;
export enum ActivepiecesVendorEventName {
VENDOR_INIT = 'VENDOR_INIT',
VENDOR_ROUTE_CHANGED = 'VENDOR_ROUTE_CHANGED',
}
export interface ActivepiecesVendorRouteChanged {
type: ActivepiecesVendorEventName.VENDOR_ROUTE_CHANGED;
data: {
vendorRoute: string;
};
}
export interface ActivepiecesVendorInit {
type: ActivepiecesVendorEventName.VENDOR_INIT;
data: {
hideSidebar: boolean;
hideFlowNameInBuilder?: boolean;
disableNavigationInBuilder: boolean | 'keep_home_button_only';
hideFolders?: boolean;
sdkVersion?: string;
jwtToken: string;
initialRoute?: string
fontUrl?: string;
fontFamily?: string;
hideExportAndImportFlow?: boolean;
hideDuplicateFlow?: boolean;
homeButtonIcon?: 'back' | 'logo';
emitHomeButtonClickedEvent?: boolean;
locale?: string;
mode?: 'light' | 'dark';
hideFlowsPageNavbar?: boolean;
hidePageHeader?: boolean;
};
}
type newWindowFeatures = {
height?: number,
width?: number,
top?: number,
left?: number,
}
type EmbeddingParam = {
containerId?: string;
styling?: {
fontUrl?: string;
fontFamily?: string;
mode?: 'light' | 'dark';
};
locale?:string;
builder?: {
disableNavigation?: boolean;
hideFlowName?: boolean;
homeButtonIcon: 'back' | 'logo';
homeButtonClickedHandler?: (data: {
route: string;
}) => void;
};
dashboard?: {
hideSidebar?: boolean;
hideFlowsPageNavbar?: boolean;
hidePageHeader?: boolean;
};
hideExportAndImportFlow?: boolean;
hideDuplicateFlow?: boolean;
hideFolders?: boolean;
navigation?: {
handler?: (data: { route: string }) => void;
}
}
type ConfigureParams = {
instanceUrl: string;
jwtToken: string;
prefix?: string;
embedding?: EmbeddingParam;
}
type RequestMethod = Required<Parameters<typeof fetch>>[1]['method'];
class ActivepiecesEmbedded {
readonly _sdkVersion = "0.8.1";
//used for Automatically Sync URL feature i.e /org/1234
_prefix = '/';
_instanceUrl = '';
//this is used to authenticate embedding for the first time
_jwtToken = '';
_resolveNewConnectionDialogClosed?: (result: ActivepiecesNewConnectionDialogClosed['data']) => void;
_dashboardAndBuilderIframeWindow?: Window;
_rejectNewConnectionDialogClosed?: (error: unknown) => void;
_handleVendorNavigation?: (data: { route: string }) => void;
_handleClientNavigation?: (data: { route: string }) => void;
_parentOrigin = window.location.origin;
readonly _MAX_CONTAINER_CHECK_COUNT = 100;
readonly _HUNDRED_MILLISECONDS = 100;
_embeddingAuth?: {
//this is used to do authentication with the backend
userJwtToken:string,
platformId:string,
projectId:string
};
_embeddingState?: EmbeddingParam;
configure({
jwtToken,
instanceUrl,
embedding,
prefix,
}: ConfigureParams) {
this._instanceUrl = this._removeTrailingSlashes(instanceUrl);
this._jwtToken = jwtToken;
this._prefix = this._removeTrailingSlashes(this._prependForwardSlashToRoute(prefix ?? '/'));
this._embeddingState = embedding;
if (embedding?.containerId) {
return this._initializeBuilderAndDashboardIframe({
containerSelector: `#${embedding.containerId}`
});
}
return new Promise((resolve) => { resolve({ status: "success" }) });
}
private _initializeBuilderAndDashboardIframe = ({
containerSelector
}: {
containerSelector: string
}) => {
return new Promise((resolve, reject) => {
this._addGracePeriodBeforeMethod({
condition: () => {
return !!document.querySelector(containerSelector);
},
method: () => {
const iframeContainer = document.querySelector(containerSelector);
if (iframeContainer) {
const iframeWindow = this.connectToEmbed({
iframeContainer,
callbackAfterConfigurationFinished: () => {
resolve({ status: "success" });
},
initialRoute: '/'
}).contentWindow;
this._dashboardAndBuilderIframeWindow = iframeWindow;
this._checkForClientRouteChanges(iframeWindow);
this._checkForBuilderHomeButtonClicked(iframeWindow);
}
else {
reject({
status: "error",
error: {
message: 'container not found',
},
});
}
},
errorMessage: 'container not found',
});
});
};
private _setupInitialMessageHandler(targetWindow: Window, initialRoute: string, callbackAfterConfigurationFinished?: () => void) {
const initialMessageHandler = (event: MessageEvent<ActivepiecesClientEvent>) => {
if (event.source === targetWindow && event.origin === new URL(this._instanceUrl).origin) {
switch (event.data.type) {
case ActivepiecesClientEventName.CLIENT_INIT: {
const apEvent: ActivepiecesVendorInit = {
type: ActivepiecesVendorEventName.VENDOR_INIT,
data: {
hideSidebar: this._embeddingState?.dashboard?.hideSidebar ?? false,
hideFlowsPageNavbar: this._embeddingState?.dashboard?.hideFlowsPageNavbar ?? false,
disableNavigationInBuilder: this._embeddingState?.builder?.disableNavigation ?? false,
hideFolders: this._embeddingState?.hideFolders ?? false,
hideFlowNameInBuilder: this._embeddingState?.builder?.hideFlowName ?? false,
jwtToken: this._jwtToken,
initialRoute,
fontUrl: this._embeddingState?.styling?.fontUrl,
fontFamily: this._embeddingState?.styling?.fontFamily,
hideExportAndImportFlow: this._embeddingState?.hideExportAndImportFlow ?? false,
emitHomeButtonClickedEvent: this._embeddingState?.builder?.homeButtonClickedHandler !== undefined,
locale: this._embeddingState?.locale ?? 'en',
sdkVersion: this._sdkVersion,
homeButtonIcon: this._embeddingState?.builder?.homeButtonIcon ?? 'logo',
hideDuplicateFlow: this._embeddingState?.hideDuplicateFlow ?? false,
mode: this._embeddingState?.styling?.mode,
hidePageHeader: this._embeddingState?.dashboard?.hidePageHeader ?? false,
},
};
targetWindow.postMessage(apEvent, '*');
this._createAuthenticationSuccessListener(targetWindow);
this._createAuthenticationFailedListener(targetWindow);
this._createConfigurationFinishedListener(targetWindow, callbackAfterConfigurationFinished);
window.removeEventListener('message', initialMessageHandler);
break;
}
}
}
};
window.addEventListener('message', initialMessageHandler);
}
private connectToEmbed({ iframeContainer, initialRoute, callbackAfterConfigurationFinished }: {
iframeContainer: Element,
initialRoute: string,
callbackAfterConfigurationFinished?: () => void
}): IframeWithWindow {
const iframe = this._createIframe({ src: `${this._instanceUrl}/embed?currentDate=${Date.now()}` });
iframeContainer.appendChild(iframe);
if (!this._doesFrameHaveWindow(iframe)) {
this._errorCreator('iframe window not accessible');
}
const iframeWindow = iframe.contentWindow;
this._setupInitialMessageHandler(iframeWindow, initialRoute, callbackAfterConfigurationFinished);
return iframe;
}
private _createConfigurationFinishedListener = (targetWindow: Window, callbackAfterConfigurationFinished?: () => void) => {
const configurationFinishedHandler = (event: MessageEvent<ActivepiecesClientConfigurationFinished>) => {
if (event.data.type === ActivepiecesClientEventName.CLIENT_CONFIGURATION_FINISHED && event.source === targetWindow) {
this._logger().log('Configuration finished')
if (callbackAfterConfigurationFinished) {
callbackAfterConfigurationFinished();
}
}
}
window.addEventListener('message', configurationFinishedHandler);
}
private _createAuthenticationFailedListener = (targetWindow: Window) => {
const authenticationFailedHandler = (event: MessageEvent<ActivepiecesClientAuthenticationFailed>) => {
if (event.data.type === ActivepiecesClientEventName.CLIENT_AUTHENTICATION_FAILED && event.source === targetWindow) {
this._errorCreator('Authentication failed',event.data.data);
}
}
window.addEventListener('message', authenticationFailedHandler);
}
private _createAuthenticationSuccessListener = (targetWindow: Window) => {
const authenticationSuccessHandler = (event: MessageEvent<ActivepiecesClientAuthenticationSuccess>) => {
if (event.data.type === ActivepiecesClientEventName.CLIENT_AUTHENTICATION_SUCCESS && event.source === targetWindow) {
this._logger().log('Authentication success')
window.removeEventListener('message', authenticationSuccessHandler);
}
}
window.addEventListener('message', authenticationSuccessHandler);
}
private _createIframe({ src }: { src: string }) {
const iframe = document.createElement('iframe');
iframe.src = src;
iframe.setAttribute('allow', 'clipboard-read; clipboard-write');
return iframe;
}
private _getNewWindowFeatures(requestedFeats:newWindowFeatures) {
const windowFeats:newWindowFeatures = {
height: 700,
width: 700,
top: 0,
left: 0,
}
Object.keys(windowFeats).forEach((key) => {
if(typeof requestedFeats === 'object' && requestedFeats[key as keyof newWindowFeatures]){
windowFeats[key as keyof newWindowFeatures ] = requestedFeats[key as keyof typeof requestedFeats]
}
})
return `width=${windowFeats.width},height=${windowFeats.height},top=${windowFeats.top},left=${windowFeats.left}`
}
private _addConnectionIframe({pieceName, connectionName}:{pieceName:string, connectionName?:string}) {
const connectionsIframe = this.connectToEmbed({
iframeContainer: document.body,
initialRoute: `/embed/connections?${NEW_CONNECTION_QUERY_PARAMS.name}=${pieceName}&randomId=${Date.now()}&${NEW_CONNECTION_QUERY_PARAMS.connectionName}=${connectionName || ''}`
});
connectionsIframe.style.cssText = ['display:none', 'position:fixed', 'top:0', 'left:0', 'width:100%', 'height:100%', 'border:none'].join(';');
return connectionsIframe;
}
private _openNewWindowForConnections({pieceName, connectionName,newWindow}:{pieceName:string, connectionName?:string, newWindow:newWindowFeatures}) {
const popup = window.open(`${this._instanceUrl}/embed`, '_blank', this._getNewWindowFeatures(newWindow));
if (!popup) {
this._errorCreator('Failed to open popup window');
}
this._setupInitialMessageHandler(popup, `/embed/connections?${NEW_CONNECTION_QUERY_PARAMS.name}=${pieceName}&randomId=${Date.now()}&${NEW_CONNECTION_QUERY_PARAMS.connectionName}=${connectionName || ''}`);
return popup;
}
async connect({ pieceName, connectionName, newWindow }: {
pieceName: string,
connectionName?: string,
newWindow?:{
height?: number,
width?: number,
top?: number,
left?: number,
}
}) {
this._cleanConnectionIframe();
return this._addGracePeriodBeforeMethod({
condition: () => {
return !!document.body;
},
method: async () => {
const target = newWindow? this._openNewWindowForConnections({pieceName, connectionName,newWindow}) : this._addConnectionIframe({pieceName, connectionName});
//don't check for window because (instanceof Window) is false for popups
if(!(target instanceof HTMLIFrameElement)) {
const checkClosed = setInterval(() => {
if (target.closed) {
clearInterval(checkClosed);
if(this._resolveNewConnectionDialogClosed) {
this._resolveNewConnectionDialogClosed({connection:undefined})
}
}
}, 500);
}
return new Promise<ActivepiecesNewConnectionDialogClosed['data']>((resolve, reject) => {
this._resolveNewConnectionDialogClosed = resolve;
this._rejectNewConnectionDialogClosed = reject;
this._setConnectionIframeEventsListener(target);
});
},
errorMessage: 'unable to add connection embedding'
});
}
navigate({ route }: { route: string }) {
if (!this._dashboardAndBuilderIframeWindow) {
this._logger().error('dashboard iframe not found');
return;
}
const event: ActivepiecesVendorRouteChanged = {
type: ActivepiecesVendorEventName.VENDOR_ROUTE_CHANGED,
data: {
vendorRoute: this._prependForwardSlashToRoute(route),
},
};
this._dashboardAndBuilderIframeWindow.postMessage(event, '*');
}
private _prependForwardSlashToRoute(route: string) {
return route.startsWith('/') ? route : `/${route}`;
}
private _checkForClientRouteChanges = (source: Window) => {
window.addEventListener(
'message',
(event: MessageEvent<ActivepiecesClientRouteChanged>) => {
if (
event.data.type ===
ActivepiecesClientEventName.CLIENT_ROUTE_CHANGED &&
event.source === source &&
this._embeddingState?.navigation?.handler
) {
const routeWithPrefix = this._prefix + this._prependForwardSlashToRoute(event.data.data.route);
this._embeddingState.navigation.handler({ route: routeWithPrefix });
return;
}
}
);
};
private _checkForBuilderHomeButtonClicked = (source: Window) => {
window.addEventListener('message', (event: MessageEvent<ActivepiecesBuilderHomeButtonClicked>) => {
if (event.data.type === ActivepiecesClientEventName.CLIENT_BUILDER_HOME_BUTTON_CLICKED && event.source === source) {
this._embeddingState?.builder?.homeButtonClickedHandler?.(event.data.data);
}
});
}
private _extractRouteAfterPrefix(vendorUrl: string, parentOriginWithPrefix: string) {
return vendorUrl.split(parentOriginWithPrefix)[1];
}
//used for Automatically Sync URL feature
extractActivepiecesRouteFromUrl({ vendorUrl }: { vendorUrl: string }) {
return this._extractRouteAfterPrefix(vendorUrl, this._removeTrailingSlashes(this._parentOrigin) + this._prefix);
}
private _doesFrameHaveWindow(
frame: HTMLIFrameElement
): frame is IframeWithWindow {
return frame.contentWindow !== null;
}
// eslint-disable-next-line @typescript-eslint/no-empty-function
private _cleanConnectionIframe = () => { };
private _setConnectionIframeEventsListener(target: Window | HTMLIFrameElement ) {
const connectionRelatedMessageHandler = (event: MessageEvent<ActivepiecesNewConnectionDialogClosed | ActivepiecesClientConnectionNameIsInvalid | ActivepiecesClientShowConnectionIframe | ActivepiecesClientConnectionPieceNotFound>) => {
if (event.data.type) {
switch (event.data.type) {
case ActivepiecesClientEventName.CLIENT_NEW_CONNECTION_DIALOG_CLOSED: {
if (this._resolveNewConnectionDialogClosed) {
this._resolveNewConnectionDialogClosed(event.data.data);
}
this._removeEmbedding(target);
window.removeEventListener('message', connectionRelatedMessageHandler);
break;
}
case ActivepiecesClientEventName.CLIENT_CONNECTION_NAME_IS_INVALID:
case ActivepiecesClientEventName.CLIENT_CONNECTION_PIECE_NOT_FOUND: {
this._removeEmbedding(target);
if (this._rejectNewConnectionDialogClosed) {
this._rejectNewConnectionDialogClosed(event.data.data);
}
else {
this._errorCreator(event.data.data.error);
}
window.removeEventListener('message', connectionRelatedMessageHandler);
break;
}
case ActivepiecesClientEventName.CLIENT_SHOW_CONNECTION_IFRAME: {
if (target instanceof HTMLIFrameElement) {
target.style.display = 'block';
}
break;
}
}
}
}
window.addEventListener(
'message',
connectionRelatedMessageHandler
);
this._cleanConnectionIframe = () => {
window.removeEventListener('message', connectionRelatedMessageHandler);
this._resolveNewConnectionDialogClosed = undefined;
this._rejectNewConnectionDialogClosed = undefined;
this._removeEmbedding(target);
}
}
private _removeTrailingSlashes(str: string) {
return str.endsWith('/') ? str.slice(0, -1) : str;
}
private _removeStartingSlashes(str: string) {
return str.startsWith('/') ? str.slice(1) : str;
}
/**Adds a grace period before executing the method depending on the condition */
private _addGracePeriodBeforeMethod({
method,
condition,
errorMessage,
}: {
method: () => Promise<any> | void;
condition: () => boolean;
/**Error message to show when grace period passes */
errorMessage: string;
}) {
return new Promise((resolve, reject) => {
let checkCounter = 0;
if (condition()) {
resolve(method());
return;
}
const checker = setInterval(() => {
if (checkCounter >= this._MAX_CONTAINER_CHECK_COUNT) {
this._logger().error(errorMessage);
reject(errorMessage);
return;
}
checkCounter++;
if (condition()) {
clearInterval(checker);
resolve(method());
}
}, this._HUNDRED_MILLISECONDS);
},);
}
private _errorCreator(message: string,...args:any[]): never {
this._logger().error(message,...args)
throw new Error(`Activepieces: ${message}`,);
}
private _removeEmbedding(target:HTMLIFrameElement | Window) {
if (target) {
if (target instanceof HTMLIFrameElement) {
target.remove();
} else {
target.close();
}
}
else {
this._logger().warn(`couldn't remove embedding`)
}
}
private _logger() {
return{
log: (message: string, ...args: any[]) => {
console.log(`Activepieces: ${message}`, ...args)
},
error: (message: string, ...args: any[]) => {
console.error(`Activepieces: ${message}`, ...args)
},
warn: (message: string, ...args: any[]) => {
console.warn(`Activepieces: ${message}`, ...args)
}
}
}
private async fetchEmbeddingAuth(params:{jwtToken:string} | undefined) {
if(this._embeddingAuth) {
return this._embeddingAuth;
}
const jwtToken = params?.jwtToken?? this._jwtToken;
if(!jwtToken) {
this._errorCreator('jwt token not found');
}
const response = await this.request({path: '/managed-authn/external-token', method: 'POST', body: {
externalAccessToken: jwtToken,
}}, false)
this._embeddingAuth = {
userJwtToken: response.token,
platformId: response.platformId,
projectId: response.projectId,
}
return this._embeddingAuth;
}
async request({path, method, body, queryParams}:{path:string, method: RequestMethod, body?:Record<string, unknown>, queryParams?:Record<string, string>}, useJwtToken = true) {
const headers:Record<string, string> = {
}
if(body) {
headers['Content-Type'] = 'application/json'
}
if(useJwtToken) {
const embeddingAuth = await this.fetchEmbeddingAuth({jwtToken: this._jwtToken});
headers['Authorization'] = `Bearer ${embeddingAuth.userJwtToken}`
}
const queryParamsString = queryParams ? `?${new URLSearchParams(queryParams).toString()}` : '';
return fetch(`${this._removeTrailingSlashes(this._instanceUrl)}/api/v1/${this._removeStartingSlashes(path)}${queryParamsString}`, {
method,
body: body ? JSON.stringify(body) : undefined,
headers,
}).then(res => res.json())
}
}
(window as any).activepieces = new ActivepiecesEmbedded();
(window as any).ActivepiecesEmbedded = ActivepiecesEmbedded;

View File

@@ -0,0 +1,19 @@
{
"extends": "../../../../tsconfig.base.json",
"compilerOptions": {
"module": "amd",
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
}
]
}

View File

@@ -0,0 +1,10 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../../../dist/out-tsc",
"declaration": true,
"types": ["node"]
},
"include": ["src/**/*.ts"],
"exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"]
}

View File

@@ -0,0 +1,5 @@
const { composePlugins, withNx } = require('@nx/webpack');
module.exports = composePlugins(withNx(), (config) => {
return config;
});

View File

@@ -0,0 +1,21 @@
{
"extends": [
"../server/api/.eslintrc.json"
],
"overrides": [
{
"files": [
"*.ts",
"*.js"
],
"parserOptions": {
"project": [
"packages/engine/tsconfig.*?.json"
]
},
"rules": {
"no-console": "off"
}
}
]
}

View File

@@ -0,0 +1,11 @@
# engine
This library was generated with [Nx](https://nx.dev).
## Building
Run `nx build engine` to build the library.
## Running unit tests
Run `nx test engine` to execute the unit tests via [Jest](https://jestjs.io).

View File

@@ -0,0 +1,23 @@
process.env.AP_EXECUTION_MODE = 'UNSANDBOXED'
process.env.AP_BASE_CODE_DIRECTORY = 'packages/engine/test/resources/codes'
process.env.AP_TEST_MODE = 'true'
process.env.AP_DEV_PIECES = 'http,data-mapper,approval,webhook'
/* eslint-disable */
export default {
displayName: 'engine',
preset: '../../jest.preset.js',
testEnvironment: 'node',
transform: {
'^.+\\.[tj]s$': [
'ts-jest',
{
tsconfig: '<rootDir>/tsconfig.spec.json',
},
],
},
transformIgnorePatterns: ["node_modules/(?!string\-replace\-async)"],
moduleFileExtensions: ['ts', 'js', 'html', 'node'],
coverageDirectory: '../../coverage/packages/engine',
};

View File

@@ -0,0 +1,6 @@
{
"name": "@activepieces/engine",
"version": "0.7.0",
"type": "commonjs"
}

View File

@@ -0,0 +1,85 @@
{
"name": "engine",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "packages/engine/src",
"projectType": "library",
"tags": [],
"targets": {
"build": {
"executor": "@nx/webpack:webpack",
"outputs": [
"{options.outputPath}"
],
"options": {
"target": "node",
"compiler": "tsc",
"outputPath": "dist/packages/engine",
"main": "packages/engine/src/main.ts",
"tsConfig": "packages/engine/tsconfig.lib.json",
"assets": [],
"webpackConfig": "packages/engine/webpack.config.js",
"babelUpwardRootMode": true,
"statsJson": false,
"sourceMap": true
},
"configurations": {
"production": {
"optimization": true,
"extractLicenses": true,
"inspect": false,
"statsJson": false
}
}
},
"serve": {
"executor": "@nx/js:node",
"options": {
"buildTarget": "engine:build",
"inspect": false
}
},
"publish": {
"executor": "nx:run-commands",
"options": {
"command": "node tools/scripts/publish.mjs engine {args.ver} {args.tag}"
},
"dependsOn": [
"build"
]
},
"lint": {
"executor": "@nx/eslint:lint",
"outputs": [
"{options.outputFile}"
],
"options": {
"lintFilePatterns": [
"packages/engine/**/*.ts"
]
}
},
"build-pieces-for-testing": {
"executor": "nx:run-commands",
"options": {
"commands": [
"npx nx run-many --target=build --projects=pieces-http,pieces-data-mapper,pieces-approval,pieces-webhook,shared"
],
"parallel": false
}
},
"test": {
"executor": "@nx/jest:jest",
"outputs": [
"{workspaceRoot}/coverage/{projectRoot}"
],
"options": {
"jestConfig": "packages/engine/jest.config.ts",
"silent": true
},
"dependsOn": [
"build-pieces-for-testing",
"build"
]
}
}
}

View File

@@ -0,0 +1,47 @@
export type CodeModule = {
code(input: unknown): Promise<unknown>
}
export type CodeSandbox = {
/**
* Executes a {@link CodeModule}.
*/
runCodeModule(params: RunCodeModuleParams): Promise<unknown>
/**
* Executes a script.
*/
runScript(params: RunScriptParams): Promise<unknown>
}
type RunCodeModuleParams = {
/**
* The {@link CodeModule} to execute.
*/
codeModule: CodeModule
/**
* The inputs that are passed to the {@link CodeModule}.
*/
inputs: Record<string, unknown>
}
type RunScriptParams = {
/**
* A serialized script that will be executed in the sandbox.
* The script can either be sync or async.
*/
script: string
/**
* A key-value map of variables available to the script during execution.
*/
scriptContext: Record<string, unknown>
/**
* A key-value map of functions that are available to the script during execution.
*/
// eslint-disable-next-line @typescript-eslint/ban-types
functions: Record<string, Function>
}

View File

@@ -0,0 +1,39 @@
import { EngineGenericError, ExecutionMode, isNil } from '@activepieces/shared'
import { CodeSandbox } from '../../core/code/code-sandbox-common'
export const EXECUTION_MODE = (process.env.AP_EXECUTION_MODE as ExecutionMode)
const loadNoOpCodeSandbox = async (): Promise<CodeSandbox> => {
const noOpCodeSandboxModule = await import('./no-op-code-sandbox')
return noOpCodeSandboxModule.noOpCodeSandbox
}
const loadV8IsolateSandbox = async (): Promise<CodeSandbox> => {
const v8IsolateCodeSandboxModule = await import('./v8-isolate-code-sandbox')
return v8IsolateCodeSandboxModule.v8IsolateCodeSandbox
}
const loadCodeSandbox = async (): Promise<CodeSandbox> => {
const loaders = {
[ExecutionMode.UNSANDBOXED]: loadNoOpCodeSandbox,
[ExecutionMode.SANDBOX_PROCESS]: loadNoOpCodeSandbox,
[ExecutionMode.SANDBOX_CODE_ONLY]: loadV8IsolateSandbox,
[ExecutionMode.SANDBOX_CODE_AND_PROCESS]: loadV8IsolateSandbox,
}
if (isNil(EXECUTION_MODE)) {
throw new EngineGenericError('ExecutionModeNotSetError', 'AP_EXECUTION_MODE environment variable is not set')
}
const loader = loaders[EXECUTION_MODE]
return loader()
}
let instance: CodeSandbox | null = null
export const initCodeSandbox = async (): Promise<CodeSandbox> => {
if (instance === null) {
instance = await loadCodeSandbox()
}
return instance
}

View File

@@ -0,0 +1,22 @@
import { CodeSandbox } from '../../core/code/code-sandbox-common'
/**
* Runs code without a sandbox.
*/
export const noOpCodeSandbox: CodeSandbox = {
async runCodeModule({ codeModule, inputs }) {
return codeModule.code(inputs)
},
async runScript({ script, scriptContext, functions }) {
const newContext = {
...scriptContext,
...functions,
}
const params = Object.keys(newContext)
const args = Object.values(newContext)
const body = `return (${script})`
const fn = Function(...params, body)
return fn(...args)
},
}

View File

@@ -0,0 +1,111 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { CodeModule, CodeSandbox } from '../../core/code/code-sandbox-common'
const ONE_HUNDRED_TWENTY_EIGHT_MEGABYTES = 128
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// Check this https://github.com/laverdet/isolated-vm/issues/258#issuecomment-2134341086
let ivmCache: any
const getIvm = () => {
if (!ivmCache) {
ivmCache = require('isolated-vm')
}
return ivmCache as typeof import('isolated-vm')
}
/**
* Runs code in a V8 Isolate sandbox
*/
export const v8IsolateCodeSandbox: CodeSandbox = {
async runCodeModule({ codeModule, inputs }) {
const ivm = getIvm()
const isolate = new ivm.Isolate({ memoryLimit: ONE_HUNDRED_TWENTY_EIGHT_MEGABYTES })
try {
const isolateContext = await initIsolateContext({
isolate,
codeContext: {
inputs,
},
})
const serializedCodeModule = serializeCodeModule(codeModule)
return await executeIsolate({
isolate,
isolateContext,
code: serializedCodeModule,
})
}
finally {
isolate.dispose()
}
},
async runScript({ script, scriptContext, functions }) {
const ivm = getIvm()
const isolate = new ivm.Isolate({ memoryLimit: ONE_HUNDRED_TWENTY_EIGHT_MEGABYTES })
try {
// It is to avoid strucutedClone issue of proxy objects / functions, It will throw cannot be cloned error.
const isolateContext = await initIsolateContext({
isolate,
codeContext: JSON.parse(JSON.stringify(scriptContext)),
})
const serializedFunctions = Object.entries(functions).map(([key, value]) => `const ${key} = ${value.toString()};`).join('\n')
const scriptWithFunctions = `${serializedFunctions}\n${script}`
return await executeIsolate({
isolate,
isolateContext,
code: scriptWithFunctions,
})
}
finally {
isolate.dispose()
}
},
}
const initIsolateContext = async ({ isolate, codeContext }: InitContextParams): Promise<any> => {
const isolateContext = await isolate.createContext()
const ivm = getIvm()
for (const [key, value] of Object.entries(codeContext)) {
await isolateContext.global.set(key, new ivm.ExternalCopy(value).copyInto())
}
return isolateContext
}
const executeIsolate = async ({ isolate, isolateContext, code }: ExecuteIsolateParams): Promise<unknown> => {
const isolateScript = await isolate.compileScript(code)
const outRef = await isolateScript.run(isolateContext, {
reference: true,
promise: true,
})
return outRef.copy()
}
const serializeCodeModule = (codeModule: CodeModule): string => {
const serializedCodeFunction = Object.keys(codeModule)
.reduce((acc, key) =>
acc + `const ${key} = ${(codeModule as any)[key].toString()};`,
'')
// replace the exports.function_name with function_name
return serializedCodeFunction.replace(/\(0, exports\.(\w+)\)/g, '$1') + 'code(inputs);'
}
type InitContextParams = {
isolate: any
codeContext: Record<string, unknown>
}
type ExecuteIsolateParams = {
isolate: any
isolateContext: unknown
code: string
}

View File

@@ -0,0 +1,13 @@
import { FlowAction } from '@activepieces/shared'
import { EngineConstants } from './context/engine-constants'
import { FlowExecutorContext } from './context/flow-execution-context'
export type ActionHandler<T extends FlowAction> = (request: { action: T, executionState: FlowExecutorContext, constants: EngineConstants }) => Promise<FlowExecutorContext>
export type BaseExecutor<T extends FlowAction> = {
handle(request: {
action: T
executionState: FlowExecutorContext
constants: EngineConstants
}): Promise<FlowExecutorContext>
}

View File

@@ -0,0 +1,77 @@
import path from 'path'
import importFresh from '@activepieces/import-fresh-webpack'
import { LATEST_CONTEXT_VERSION } from '@activepieces/pieces-framework'
import { CodeAction, EngineGenericError, FlowActionType, FlowRunStatus, GenericStepOutput, isNil, StepOutputStatus } from '@activepieces/shared'
import { initCodeSandbox } from '../core/code/code-sandbox'
import { CodeModule } from '../core/code/code-sandbox-common'
import { continueIfFailureHandler, runWithExponentialBackoff } from '../helper/error-handling'
import { progressService } from '../services/progress.service'
import { utils } from '../utils'
import { ActionHandler, BaseExecutor } from './base-executor'
export const codeExecutor: BaseExecutor<CodeAction> = {
async handle({
action,
executionState,
constants,
}) {
if (executionState.isCompleted({ stepName: action.name })) {
return executionState
}
const resultExecution = await runWithExponentialBackoff(executionState, action, constants, executeAction)
return continueIfFailureHandler(resultExecution, action, constants)
},
}
const executeAction: ActionHandler<CodeAction> = async ({ action, executionState, constants }) => {
const stepStartTime = performance.now()
const { censoredInput, resolvedInput } = await constants.getPropsResolver(LATEST_CONTEXT_VERSION).resolve<Record<string, unknown>>({
unresolvedInput: action.settings.input,
executionState,
})
const stepOutput = GenericStepOutput.create({
input: censoredInput,
type: FlowActionType.CODE,
status: StepOutputStatus.RUNNING,
})
const { data: executionStateResult, error: executionStateError } = await utils.tryCatchAndThrowOnEngineError((async () => {
await progressService.sendUpdate({
engineConstants: constants,
flowExecutorContext: executionState.upsertStep(action.name, stepOutput),
})
if (isNil(constants.runEnvironment)) {
throw new EngineGenericError('RunEnvironmentNotSetError', 'Run environment is not set')
}
const artifactPath = path.resolve(`${constants.baseCodeDirectory}/${constants.flowVersionId}/${action.name}/index.js`)
const codeModule: CodeModule = await importFresh(artifactPath)
const codeSandbox = await initCodeSandbox()
const output = await codeSandbox.runCodeModule({
codeModule,
inputs: resolvedInput,
})
return executionState.upsertStep(action.name, stepOutput.setOutput(output).setStatus(StepOutputStatus.SUCCEEDED).setDuration(performance.now() - stepStartTime)).incrementStepsExecuted()
}))
if (executionStateError) {
const failedStepOutput = stepOutput
.setStatus(StepOutputStatus.FAILED)
.setErrorMessage(utils.formatError(executionStateError))
.setDuration(performance.now() - stepStartTime)
return executionState
.upsertStep(action.name, failedStepOutput)
.setVerdict({ status: FlowRunStatus.FAILED, failedStep: {
name: action.name,
displayName: action.displayName,
message: utils.formatError(executionStateError),
} })
}
return executionStateResult
}

View File

@@ -0,0 +1,241 @@
import { ContextVersion } from '@activepieces/pieces-framework'
import { DEFAULT_MCP_DATA, EngineGenericError, ExecuteFlowOperation, ExecutePropsOptions, ExecuteToolOperation, ExecuteTriggerOperation, ExecutionType, FlowVersionState, PlatformId, ProgressUpdateType, Project, ProjectId, ResumePayload, RunEnvironment, TriggerHookType } from '@activepieces/shared'
import { createPropsResolver, PropsResolver } from '../../variables/props-resolver'
type RetryConstants = {
maxAttempts: number
retryExponential: number
retryInterval: number
}
type EngineConstantsParams = {
flowId: string
flowVersionId: string
flowVersionState: FlowVersionState
triggerPieceName: string
flowRunId: string
publicApiUrl: string
internalApiUrl: string
retryConstants: RetryConstants
engineToken: string
projectId: ProjectId
progressUpdateType: ProgressUpdateType
serverHandlerId: string | null
httpRequestId: string | null
resumePayload?: ResumePayload
runEnvironment?: RunEnvironment
stepNameToTest?: string
logsUploadUrl?: string
logsFileId?: string
timeoutInSeconds: number
platformId: PlatformId
}
const DEFAULT_RETRY_CONSTANTS: RetryConstants = {
maxAttempts: 4,
retryExponential: 2,
retryInterval: 2000,
}
const DEFAULT_TRIGGER_EXECUTION = 'execute-trigger'
const DEFAULT_EXECUTE_PROPERTY = 'execute-property'
export class EngineConstants {
public static readonly BASE_CODE_DIRECTORY = process.env.AP_BASE_CODE_DIRECTORY ?? './codes'
public static readonly INPUT_FILE = './input.json'
public static readonly OUTPUT_FILE = './output.json'
public static readonly DEV_PIECES = process.env.AP_DEV_PIECES?.split(',') ?? []
public static readonly TEST_MODE = process.env.AP_TEST_MODE === 'true'
public readonly platformId: string
public readonly timeoutInSeconds: number
public readonly flowId: string
public readonly flowVersionId: string
public readonly flowVersionState: FlowVersionState
public readonly triggerPieceName: string
public readonly flowRunId: string
public readonly publicApiUrl: string
public readonly internalApiUrl: string
public readonly retryConstants: RetryConstants
public readonly engineToken: string
public readonly projectId: ProjectId
public readonly progressUpdateType: ProgressUpdateType
public readonly serverHandlerId: string | null
public readonly httpRequestId: string | null
public readonly resumePayload?: ResumePayload
public readonly runEnvironment?: RunEnvironment
public readonly stepNameToTest?: string
public readonly logsUploadUrl?: string
public readonly logsFileId?: string
private project: Project | null = null
public get isRunningApTests(): boolean {
return EngineConstants.TEST_MODE
}
public get baseCodeDirectory(): string {
return EngineConstants.BASE_CODE_DIRECTORY
}
public get devPieces(): string[] {
return EngineConstants.DEV_PIECES
}
public constructor(params: EngineConstantsParams) {
if (!params.publicApiUrl.endsWith('/api/')) {
throw new EngineGenericError('PublicUrlNotEndsWithSlashError', `Public URL must end with a slash, got: ${params.publicApiUrl}`)
}
if (!params.internalApiUrl.endsWith('/')) {
throw new EngineGenericError('InternalApiUrlNotEndsWithSlashError', `Internal API URL must end with a slash, got: ${params.internalApiUrl}`)
}
this.flowId = params.flowId
this.flowVersionId = params.flowVersionId
this.flowVersionState = params.flowVersionState
this.flowRunId = params.flowRunId
this.publicApiUrl = params.publicApiUrl
this.internalApiUrl = params.internalApiUrl
this.retryConstants = params.retryConstants
this.triggerPieceName = params.triggerPieceName
this.engineToken = params.engineToken
this.projectId = params.projectId
this.progressUpdateType = params.progressUpdateType
this.serverHandlerId = params.serverHandlerId
this.httpRequestId = params.httpRequestId
this.resumePayload = params.resumePayload
this.runEnvironment = params.runEnvironment
this.stepNameToTest = params.stepNameToTest
this.logsUploadUrl = params.logsUploadUrl
this.logsFileId = params.logsFileId
this.platformId = params.platformId
this.timeoutInSeconds = params.timeoutInSeconds
}
public static fromExecuteFlowInput(input: ExecuteFlowOperation): EngineConstants {
return new EngineConstants({
flowId: input.flowVersion.flowId,
flowVersionId: input.flowVersion.id,
flowVersionState: input.flowVersion.state,
triggerPieceName: input.flowVersion.trigger.settings.pieceName,
flowRunId: input.flowRunId,
publicApiUrl: input.publicApiUrl,
internalApiUrl: input.internalApiUrl,
retryConstants: DEFAULT_RETRY_CONSTANTS,
engineToken: input.engineToken,
projectId: input.projectId,
progressUpdateType: input.progressUpdateType,
serverHandlerId: input.serverHandlerId ?? null,
httpRequestId: input.httpRequestId ?? null,
resumePayload: input.executionType === ExecutionType.RESUME ? input.resumePayload : undefined,
runEnvironment: input.runEnvironment,
stepNameToTest: input.stepNameToTest ?? undefined,
logsUploadUrl: input.logsUploadUrl,
logsFileId: input.logsFileId,
timeoutInSeconds: input.timeoutInSeconds,
platformId: input.platformId,
})
}
public static fromExecuteActionInput(input: ExecuteToolOperation): EngineConstants {
return new EngineConstants({
flowId: DEFAULT_MCP_DATA.flowId,
flowVersionId: DEFAULT_MCP_DATA.flowVersionId,
flowVersionState: DEFAULT_MCP_DATA.flowVersionState,
triggerPieceName: DEFAULT_MCP_DATA.triggerPieceName,
flowRunId: DEFAULT_MCP_DATA.flowRunId,
publicApiUrl: input.publicApiUrl,
internalApiUrl: addTrailingSlashIfMissing(input.internalApiUrl),
retryConstants: DEFAULT_RETRY_CONSTANTS,
engineToken: input.engineToken,
projectId: input.projectId,
progressUpdateType: ProgressUpdateType.NONE,
serverHandlerId: null,
httpRequestId: null,
resumePayload: undefined,
runEnvironment: undefined,
stepNameToTest: undefined,
timeoutInSeconds: input.timeoutInSeconds,
platformId: input.platformId,
})
}
public static fromExecutePropertyInput(input: Omit<ExecutePropsOptions, 'piece'> & { pieceName: string, pieceVersion: string }): EngineConstants {
return new EngineConstants({
flowId: input.flowVersion?.flowId ?? DEFAULT_MCP_DATA.flowId,
flowVersionId: input.flowVersion?.id ?? DEFAULT_MCP_DATA.flowVersionId,
flowVersionState: input.flowVersion?.state ?? DEFAULT_MCP_DATA.flowVersionState,
triggerPieceName: input.flowVersion?.trigger?.settings.pieceName ?? DEFAULT_MCP_DATA.triggerPieceName,
flowRunId: DEFAULT_EXECUTE_PROPERTY,
publicApiUrl: input.publicApiUrl,
internalApiUrl: addTrailingSlashIfMissing(input.internalApiUrl),
retryConstants: DEFAULT_RETRY_CONSTANTS,
engineToken: input.engineToken,
projectId: input.projectId,
progressUpdateType: ProgressUpdateType.NONE,
serverHandlerId: null,
httpRequestId: null,
resumePayload: undefined,
runEnvironment: undefined,
stepNameToTest: undefined,
timeoutInSeconds: input.timeoutInSeconds,
platformId: input.platformId,
})
}
public static fromExecuteTriggerInput(input: ExecuteTriggerOperation<TriggerHookType>): EngineConstants {
return new EngineConstants({
flowId: input.flowVersion.flowId,
flowVersionId: input.flowVersion.id,
flowVersionState: input.flowVersion.state,
triggerPieceName: input.flowVersion.trigger.settings.pieceName,
flowRunId: DEFAULT_TRIGGER_EXECUTION,
publicApiUrl: input.publicApiUrl,
internalApiUrl: addTrailingSlashIfMissing(input.internalApiUrl),
retryConstants: DEFAULT_RETRY_CONSTANTS,
engineToken: input.engineToken,
projectId: input.projectId,
progressUpdateType: ProgressUpdateType.NONE,
serverHandlerId: null,
httpRequestId: null,
resumePayload: undefined,
runEnvironment: undefined,
stepNameToTest: undefined,
timeoutInSeconds: input.timeoutInSeconds,
platformId: input.platformId,
})
}
public getPropsResolver(contextVersion: ContextVersion | undefined): PropsResolver {
return createPropsResolver({
projectId: this.projectId,
engineToken: this.engineToken,
apiUrl: this.internalApiUrl,
contextVersion,
})
}
private async getProject(): Promise<Project> {
if (this.project) {
return this.project
}
const getWorkerProjectEndpoint = `${this.internalApiUrl}v1/worker/project`
const response = await fetch(getWorkerProjectEndpoint, {
headers: {
Authorization: `Bearer ${this.engineToken}`,
},
})
this.project = await response.json() as Project
return this.project
}
public externalProjectId = async (): Promise<string | undefined> => {
const project = await this.getProject()
return project.externalId
}
}
const addTrailingSlashIfMissing = (url: string): string => {
return url.endsWith('/') ? url : url + '/'
}

View File

@@ -0,0 +1,213 @@
import { assertEqual, EngineGenericError, FailedStep, FlowActionType, FlowRunStatus, GenericStepOutput, isNil, LoopStepOutput, LoopStepResult, PauseMetadata, PauseType, RespondResponse, StepOutput, StepOutputStatus } from '@activepieces/shared'
import dayjs from 'dayjs'
import { nanoid } from 'nanoid'
import { loggingUtils } from '../../helper/logging-utils'
import { StepExecutionPath } from './step-execution-path'
export type FlowVerdict = {
status: FlowRunStatus.PAUSED
pauseMetadata: PauseMetadata
} | {
status: FlowRunStatus.SUCCEEDED
stopResponse: RespondResponse | undefined
} | {
status: FlowRunStatus.FAILED
failedStep: FailedStep
} | {
status: FlowRunStatus.RUNNING
}
export class FlowExecutorContext {
tags: readonly string[]
steps: Readonly<Record<string, StepOutput>>
pauseRequestId: string
verdict: FlowVerdict
currentPath: StepExecutionPath
stepNameToTest?: boolean
stepsCount: number
/**
* Execution time in milliseconds
*/
duration: number
constructor(copyFrom?: FlowExecutorContext) {
this.tags = copyFrom?.tags ?? []
this.steps = copyFrom?.steps ?? {}
this.pauseRequestId = copyFrom?.pauseRequestId ?? nanoid()
this.duration = copyFrom?.duration ?? -1
this.verdict = copyFrom?.verdict ?? { status: FlowRunStatus.RUNNING }
this.currentPath = copyFrom?.currentPath ?? StepExecutionPath.empty()
this.stepNameToTest = copyFrom?.stepNameToTest ?? false
this.stepsCount = copyFrom?.stepsCount ?? 0
}
static empty(): FlowExecutorContext {
return new FlowExecutorContext()
}
public setPauseRequestId(pauseRequestId: string): FlowExecutorContext {
return new FlowExecutorContext({
...this,
pauseRequestId,
})
}
public getDelayedInSeconds(): number | undefined {
if (this.verdict.status === FlowRunStatus.PAUSED && this.verdict.pauseMetadata.type === PauseType.DELAY) {
return dayjs(this.verdict.pauseMetadata.resumeDateTime).diff(Date.now(), 'seconds')
}
return undefined
}
public finishExecution(): FlowExecutorContext {
if (this.verdict.status === FlowRunStatus.RUNNING) {
return new FlowExecutorContext({
...this,
verdict: { status: FlowRunStatus.SUCCEEDED },
})
}
return this
}
public trimmedSteps(): Promise<Record<string, StepOutput>> {
return loggingUtils.trimExecution(this.steps)
}
public getLoopStepOutput({ stepName }: { stepName: string }): LoopStepOutput | undefined {
const stateAtPath = getStateAtPath({ currentPath: this.currentPath, steps: this.steps })
const stepOutput = stateAtPath[stepName]
if (isNil(stepOutput)) {
return undefined
}
assertEqual(stepOutput.type, FlowActionType.LOOP_ON_ITEMS, 'stepOutput.type', 'LOOP_ON_ITEMS')
// The new LoopStepOutput is needed as casting directly to LoopClassOutput will just cast the data but the class methods will not be available
return new LoopStepOutput(stepOutput as GenericStepOutput<FlowActionType.LOOP_ON_ITEMS, LoopStepResult>)
}
public isCompleted({ stepName }: { stepName: string }): boolean {
const stateAtPath = getStateAtPath({ currentPath: this.currentPath, steps: this.steps })
const stepOutput = stateAtPath[stepName]
if (isNil(stepOutput)) {
return false
}
return stepOutput.status !== StepOutputStatus.PAUSED
}
public isPaused({ stepName }: { stepName: string }): boolean {
const stateAtPath = getStateAtPath({ currentPath: this.currentPath, steps: this.steps })
const stepOutput = stateAtPath[stepName]
if (isNil(stepOutput)) {
return false
}
return stepOutput.status === StepOutputStatus.PAUSED
}
public setDuration(duration: number): FlowExecutorContext {
return new FlowExecutorContext({
...this,
duration,
})
}
public addTags(tags: string[]): FlowExecutorContext {
return new FlowExecutorContext({
...this,
tags: [...this.tags, ...tags].filter((value, index, self) => {
return self.indexOf(value) === index
}),
})
}
public upsertStep(stepName: string, stepOutput: StepOutput): FlowExecutorContext {
const steps = {
...this.steps,
}
const targetMap = getStateAtPath({ currentPath: this.currentPath, steps })
targetMap[stepName] = stepOutput
return new FlowExecutorContext({
...this,
steps,
})
}
public getStepOutput(stepName: string): StepOutput | undefined {
const stateAtPath = getStateAtPath({ currentPath: this.currentPath, steps: this.steps })
return stateAtPath[stepName]
}
public setCurrentPath(currentStatePath: StepExecutionPath): FlowExecutorContext {
return new FlowExecutorContext({
...this,
currentPath: currentStatePath,
})
}
public setVerdict(verdict: FlowVerdict): FlowExecutorContext {
return new FlowExecutorContext({
...this,
verdict,
})
}
public setRetryable(retryable: boolean): FlowExecutorContext {
return new FlowExecutorContext({
...this,
retryable,
})
}
public incrementStepsExecuted(): FlowExecutorContext {
return new FlowExecutorContext({
...this,
stepsCount: this.stepsCount + 1,
})
}
public currentState(): Record<string, unknown> {
let flattenedSteps: Record<string, unknown> = extractOutput(this.steps)
let targetMap = this.steps
this.currentPath.path.forEach(([stepName, iteration]) => {
const stepOutput = targetMap[stepName]
if (!stepOutput.output || stepOutput.type !== FlowActionType.LOOP_ON_ITEMS) {
throw new EngineGenericError('NotInstanceOfLoopOnItemsStepOutputError', '[ExecutionState#getTargetMap] Not instance of Loop On Items step output')
}
targetMap = stepOutput.output.iterations[iteration]
flattenedSteps = {
...flattenedSteps,
...extractOutput(targetMap),
}
})
return flattenedSteps
}
}
function extractOutput(steps: Record<string, StepOutput>): Record<string, unknown> {
return Object.entries(steps).reduce((acc: Record<string, unknown>, [stepName, step]) => {
acc[stepName] = step.output
return acc
}, {} as Record<string, unknown>)
}
function getStateAtPath({ currentPath, steps }: { currentPath: StepExecutionPath, steps: Record<string, StepOutput> }): Record<string, StepOutput> {
let targetMap = steps
currentPath.path.forEach(([stepName, iteration]) => {
const stepOutput = targetMap[stepName]
if (!stepOutput.output || stepOutput.type !== FlowActionType.LOOP_ON_ITEMS) {
throw new EngineGenericError('NotInstanceOfLoopOnItemsStepOutputError', `[ExecutionState#getTargetMap] Not instance of Loop On Items step output: ${stepOutput.type}`)
}
targetMap = stepOutput.output.iterations[iteration]
})
return targetMap
}

View File

@@ -0,0 +1,20 @@
export class StepExecutionPath {
public path: readonly [string, number][] = []
constructor(path: readonly [string, number][]) {
this.path = [...path]
}
loopIteration({ loopName, iteration }: { loopName: string, iteration: number }): StepExecutionPath {
return new StepExecutionPath([...this.path, [loopName, iteration]])
}
static empty(): StepExecutionPath {
return new StepExecutionPath([])
}
removeLast(): StepExecutionPath {
const newPath = this.path.slice(0, -1)
return new StepExecutionPath(newPath)
}
}

View File

@@ -0,0 +1,99 @@
import { LATEST_CONTEXT_VERSION } from '@activepieces/pieces-framework'
import {
FlowActionType,
flowStructureUtil,
FlowTriggerType,
FlowVersion,
GenericStepOutput,
isNil,
LoopStepOutput,
RouterStepOutput,
spreadIfDefined,
StepOutputStatus,
} from '@activepieces/shared'
import { createPropsResolver } from '../../variables/props-resolver'
import { FlowExecutorContext } from './flow-execution-context'
export const testExecutionContext = {
async stateFromFlowVersion({
flowVersion,
excludedStepName,
projectId,
engineToken,
apiUrl,
sampleData,
}: TestExecutionParams): Promise<FlowExecutorContext> {
let flowExecutionContext = FlowExecutorContext.empty()
if (isNil(flowVersion)) {
return flowExecutionContext
}
const flowSteps = flowStructureUtil.getAllSteps(flowVersion.trigger)
for (const step of flowSteps) {
const { name } = step
if (name === excludedStepName) {
continue
}
const stepType = step.type
switch (stepType) {
case FlowActionType.ROUTER:
flowExecutionContext = flowExecutionContext.upsertStep(
step.name,
RouterStepOutput.create({
input: step.settings,
type: stepType,
status: StepOutputStatus.SUCCEEDED,
...spreadIfDefined('output', sampleData?.[step.name]),
}),
)
break
case FlowActionType.LOOP_ON_ITEMS: {
const { resolvedInput } = await createPropsResolver({
apiUrl,
projectId,
engineToken,
contextVersion: LATEST_CONTEXT_VERSION,
}).resolve<{ items: unknown[] }>({
unresolvedInput: step.settings,
executionState: flowExecutionContext,
})
flowExecutionContext = flowExecutionContext.upsertStep(
step.name,
LoopStepOutput.init({
input: step.settings,
}).setOutput({
item: resolvedInput.items[0],
index: 1,
iterations: [],
}),
)
break
}
case FlowActionType.PIECE:
case FlowActionType.CODE:
case FlowTriggerType.EMPTY:
case FlowTriggerType.PIECE:
flowExecutionContext = flowExecutionContext.upsertStep(step.name, GenericStepOutput.create({
input: {},
type: stepType,
status: StepOutputStatus.SUCCEEDED,
...spreadIfDefined('output', sampleData?.[step.name]),
}))
break
}
}
return flowExecutionContext
},
}
type TestExecutionParams = {
flowVersion?: FlowVersion
excludedStepName?: string
projectId: string
apiUrl: string
engineToken: string
sampleData?: Record<string, unknown>
}

View File

@@ -0,0 +1,90 @@
import { performance } from 'node:perf_hooks'
import { EngineGenericError, ExecuteFlowOperation, ExecutionType, FlowAction, FlowActionType, FlowRunStatus, isNil } from '@activepieces/shared'
import { triggerHelper } from '../helper/trigger-helper'
import { progressService } from '../services/progress.service'
import { BaseExecutor } from './base-executor'
import { codeExecutor } from './code-executor'
import { EngineConstants } from './context/engine-constants'
import { FlowExecutorContext } from './context/flow-execution-context'
import { loopExecutor } from './loop-executor'
import { pieceExecutor } from './piece-executor'
import { routerExecuter } from './router-executor'
function getExecuteFunction(): Record<FlowActionType, BaseExecutor<FlowAction>> {
return {
[FlowActionType.CODE]: codeExecutor,
[FlowActionType.LOOP_ON_ITEMS]: loopExecutor,
[FlowActionType.PIECE]: pieceExecutor,
[FlowActionType.ROUTER]: routerExecuter,
}
}
export const flowExecutor = {
getExecutorForAction(type: FlowActionType): BaseExecutor<FlowAction> {
const executeFunction = getExecuteFunction()
const executor = executeFunction[type]
if (isNil(executor)) {
throw new EngineGenericError('ExecutorNotFoundError', `Executor not found for action type: ${type}`)
}
return executor
},
async executeFromTrigger({ executionState, constants, input }: {
executionState: FlowExecutorContext
constants: EngineConstants
input: ExecuteFlowOperation
}): Promise<FlowExecutorContext> {
const trigger = input.flowVersion.trigger
if (input.executionType === ExecutionType.BEGIN) {
await triggerHelper.executeOnStart(trigger, constants, input.triggerPayload)
}
return flowExecutor.execute({
action: trigger.nextAction,
executionState,
constants,
})
},
async execute({ action, constants, executionState }: {
action: FlowAction | null | undefined
executionState: FlowExecutorContext
constants: EngineConstants
}): Promise<FlowExecutorContext> {
const flowStartTime = performance.now()
let flowExecutionContext = executionState
let currentAction: FlowAction | null | undefined = action
while (!isNil(currentAction)) {
const testSingleStepMode = !isNil(constants.stepNameToTest)
if (currentAction.skip && !testSingleStepMode) {
currentAction = currentAction.nextAction
continue
}
const handler = this.getExecutorForAction(currentAction.type)
progressService.sendUpdate({
engineConstants: constants,
flowExecutorContext: flowExecutionContext,
}).catch(error => {
console.error('Error sending update:', error)
})
flowExecutionContext = await handler.handle({
action: currentAction,
executionState: flowExecutionContext,
constants,
})
const shouldBreakExecution = flowExecutionContext.verdict.status !== FlowRunStatus.RUNNING || testSingleStepMode
if (shouldBreakExecution) {
break
}
currentAction = currentAction.nextAction
}
const flowEndTime = performance.now()
return flowExecutionContext.setDuration(flowEndTime - flowStartTime)
},
}

View File

@@ -0,0 +1,77 @@
import { LATEST_CONTEXT_VERSION } from '@activepieces/pieces-framework'
import { FlowRunStatus, isNil, LoopOnItemsAction, LoopStepOutput, StepOutputStatus } from '@activepieces/shared'
import { BaseExecutor } from './base-executor'
import { flowExecutor } from './flow-executor'
type LoopOnActionResolvedSettings = {
items: readonly unknown[]
}
export const loopExecutor: BaseExecutor<LoopOnItemsAction> = {
async handle({
action,
executionState,
constants,
}) {
const stepStartTime = performance.now()
const { resolvedInput, censoredInput } = await constants.getPropsResolver(LATEST_CONTEXT_VERSION).resolve<LoopOnActionResolvedSettings>({
unresolvedInput: {
items: action.settings.items,
},
executionState,
})
const previousStepOutput = executionState.getLoopStepOutput({ stepName: action.name })
let stepOutput = previousStepOutput ?? LoopStepOutput.init({
input: censoredInput,
})
let newExecutionContext = executionState.upsertStep(action.name, stepOutput)
if (!Array.isArray(resolvedInput.items)) {
const errorMessage = JSON.stringify({
message: 'The items you have selected must be a list.',
})
const failedStepOutput = stepOutput
.setStatus(StepOutputStatus.FAILED)
.setErrorMessage(errorMessage)
.setDuration( performance.now() - stepStartTime)
return newExecutionContext.upsertStep(action.name, failedStepOutput).setVerdict({ status: FlowRunStatus.FAILED, failedStep: {
name: action.name,
displayName: action.displayName,
message: errorMessage,
} })
}
const firstLoopAction = action.firstLoopAction
for (let i = 0; i < resolvedInput.items.length; ++i) {
const newCurrentPath = newExecutionContext.currentPath.loopIteration({ loopName: action.name, iteration: i })
const testSingleStepMode = !isNil(constants.stepNameToTest)
stepOutput = stepOutput.setItemAndIndex({ item: resolvedInput.items[i], index: i + 1 })
const addEmptyIteration = !stepOutput.hasIteration(i)
if (addEmptyIteration) {
stepOutput = stepOutput.addIteration()
}
newExecutionContext = newExecutionContext.upsertStep(action.name, stepOutput).setCurrentPath(newCurrentPath)
if (!isNil(firstLoopAction) && !testSingleStepMode) {
newExecutionContext = await flowExecutor.execute({
action: firstLoopAction,
executionState: newExecutionContext,
constants,
})
}
newExecutionContext = newExecutionContext.setCurrentPath(newExecutionContext.currentPath.removeLast())
if (newExecutionContext.verdict.status !== FlowRunStatus.RUNNING) {
return newExecutionContext.upsertStep(action.name, stepOutput.setDuration(performance.now() - stepStartTime))
}
if (testSingleStepMode) {
break
}
}
return newExecutionContext.upsertStep(action.name, stepOutput.setDuration(performance.now() - stepStartTime))
},
}

View File

@@ -0,0 +1,325 @@
import { URL } from 'url'
import { ActionContext, backwardCompatabilityContextUtils, ConstructToolParams, InputPropertyMap, PauseHook, PauseHookParams, PieceAuthProperty, PiecePropertyMap, RespondHook, RespondHookParams, StaticPropsValue, StopHook, StopHookParams, TagsManager } from '@activepieces/pieces-framework'
import { AUTHENTICATION_PROPERTY_NAME, EngineGenericError, EngineSocketEvent, ExecutionType, FlowActionType, FlowRunStatus, GenericStepOutput, isNil, PausedFlowTimeoutError, PauseType, PieceAction, RespondResponse, StepOutputStatus } from '@activepieces/shared'
import { LanguageModelV2 } from '@ai-sdk/provider'
import { ToolSet } from 'ai'
import dayjs from 'dayjs'
import { continueIfFailureHandler, runWithExponentialBackoff } from '../helper/error-handling'
import { pieceLoader } from '../helper/piece-loader'
import { createFlowsContext } from '../services/flows.service'
import { progressService } from '../services/progress.service'
import { createFilesService } from '../services/step-files.service'
import { createContextStore } from '../services/storage.service'
import { agentTools } from '../tools'
import { HookResponse, utils } from '../utils'
import { propsProcessor } from '../variables/props-processor'
import { workerSocket } from '../worker-socket'
import { ActionHandler, BaseExecutor } from './base-executor'
const AP_PAUSED_FLOW_TIMEOUT_DAYS = Number(process.env.AP_PAUSED_FLOW_TIMEOUT_DAYS)
export const pieceExecutor: BaseExecutor<PieceAction> = {
async handle({
action,
executionState,
constants,
}) {
if (executionState.isCompleted({ stepName: action.name })) {
return executionState
}
const resultExecution = await runWithExponentialBackoff(executionState, action, constants, executeAction)
return continueIfFailureHandler(resultExecution, action, constants)
},
}
const executeAction: ActionHandler<PieceAction> = async ({ action, executionState, constants }) => {
const stepStartTime = performance.now()
const stepOutput = GenericStepOutput.create({
input: {},
type: FlowActionType.PIECE,
status: StepOutputStatus.RUNNING,
})
const { data: executionStateResult, error: executionStateError } = await utils.tryCatchAndThrowOnEngineError((async () => {
if (isNil(action.settings.actionName)) {
throw new EngineGenericError('ActionNameNotSetError', 'Action name is not set')
}
const { pieceAction, piece } = await pieceLoader.getPieceAndActionOrThrow({
pieceName: action.settings.pieceName,
pieceVersion: action.settings.pieceVersion,
actionName: action.settings.actionName,
devPieces: constants.devPieces,
})
const { resolvedInput, censoredInput } = await constants.getPropsResolver(piece.getContextInfo?.().version).resolve<StaticPropsValue<PiecePropertyMap>>({
unresolvedInput: action.settings.input,
executionState,
})
stepOutput.input = censoredInput
const { processedInput, errors } = await propsProcessor.applyProcessorsAndValidators(resolvedInput, pieceAction.props, piece.auth, pieceAction.requireAuth, action.settings.propertySettings)
if (Object.keys(errors).length > 0) {
throw new Error(JSON.stringify(errors, null, 2))
}
const params: {
hookResponse: HookResponse
} = {
hookResponse: {
type: 'none',
tags: [],
},
}
const outputContext = progressService.createOutputContext({
engineConstants: constants,
flowExecutorContext: executionState,
stepName: action.name,
stepOutput,
})
const isPaused = executionState.isPaused({ stepName: action.name })
if (!isPaused) {
await progressService.sendUpdate({
engineConstants: constants,
flowExecutorContext: executionState.upsertStep(action.name, stepOutput),
})
}
const context: ActionContext<PieceAuthProperty, InputPropertyMap> = {
executionType: isPaused ? ExecutionType.RESUME : ExecutionType.BEGIN,
resumePayload: constants.resumePayload!,
store: createContextStore({
apiUrl: constants.internalApiUrl,
prefix: '',
flowId: constants.flowId,
engineToken: constants.engineToken,
}),
output: outputContext,
flows: createFlowsContext({
engineToken: constants.engineToken,
internalApiUrl: constants.internalApiUrl,
flowId: constants.flowId,
flowVersionId: constants.flowVersionId,
}),
step: {
name: action.name,
},
auth: processedInput[AUTHENTICATION_PROPERTY_NAME],
files: createFilesService({
apiUrl: constants.internalApiUrl,
engineToken: constants.engineToken,
stepName: action.name,
flowId: constants.flowId,
}),
server: {
token: constants.engineToken,
apiUrl: constants.internalApiUrl,
publicUrl: constants.publicApiUrl,
},
agent: {
tools: async (params: ConstructToolParams): Promise<ToolSet> => agentTools.tools({
engineConstants: constants,
tools: params.tools,
model: params.model as LanguageModelV2,
}),
},
propsValue: processedInput,
tags: createTagsManager(params),
connections: utils.createConnectionManager({
apiUrl: constants.internalApiUrl,
projectId: constants.projectId,
engineToken: constants.engineToken,
target: 'actions',
hookResponse: params.hookResponse,
contextVersion: piece.getContextInfo?.().version,
}),
run: {
id: constants.flowRunId,
stop: createStopHook(params),
pause: createPauseHook(params, executionState.pauseRequestId, constants.httpRequestId),
respond: createRespondHook(params),
},
project: {
id: constants.projectId,
externalId: constants.externalProjectId,
},
generateResumeUrl: (params) => {
const url = new URL(`${constants.publicApiUrl}v1/flow-runs/${constants.flowRunId}/requests/${executionState.pauseRequestId}${params.sync ? '/sync' : ''}`)
url.search = new URLSearchParams(params.queryParams).toString()
return url.toString()
},
}
const backwardCompatibleContext = backwardCompatabilityContextUtils.makeActionContextBackwardCompatible({
contextVersion: piece.getContextInfo?.().version,
context,
})
const testSingleStepMode = !isNil(constants.stepNameToTest)
const runMethodToExecute = (testSingleStepMode && !isNil(pieceAction.test)) ? pieceAction.test : pieceAction.run
const output = await runMethodToExecute(backwardCompatibleContext)
const newExecutionContext = executionState.addTags(params.hookResponse.tags)
const webhookResponse = getResponse(params.hookResponse)
const isSamePiece = constants.triggerPieceName === action.settings.pieceName
if (!isNil(webhookResponse) && !isNil(constants.serverHandlerId) && !isNil(constants.httpRequestId) && isSamePiece) {
await workerSocket.sendToWorkerWithAck(EngineSocketEvent.SEND_FLOW_RESPONSE, {
workerHandlerId: constants.serverHandlerId,
httpRequestId: constants.httpRequestId,
runResponse: {
status: webhookResponse.status ?? 200,
body: webhookResponse.body ?? {},
headers: webhookResponse.headers ?? {},
},
})
}
const stepEndTime = performance.now()
if (params.hookResponse.type === 'stopped') {
if (isNil(params.hookResponse.response)) {
throw new EngineGenericError('StopResponseNotSetError', 'Stop response is not set')
}
return newExecutionContext.upsertStep(action.name, stepOutput.setOutput(output).setStatus(StepOutputStatus.SUCCEEDED).setDuration(stepEndTime - stepStartTime)).incrementStepsExecuted().setVerdict({
status: FlowRunStatus.SUCCEEDED,
stopResponse: (params.hookResponse.response as StopHookParams).response,
})
}
if (params.hookResponse.type === 'paused') {
if (isNil(params.hookResponse.response)) {
throw new EngineGenericError('PauseResponseNotSetError', 'Pause response is not set')
}
return newExecutionContext.upsertStep(action.name, stepOutput.setOutput(output).setStatus(StepOutputStatus.PAUSED).setDuration(stepEndTime - stepStartTime)).incrementStepsExecuted()
.setVerdict({
status: FlowRunStatus.PAUSED,
pauseMetadata: (params.hookResponse.response as PauseHookParams).pauseMetadata,
})
}
return newExecutionContext.upsertStep(action.name, stepOutput.setOutput(output).setStatus(StepOutputStatus.SUCCEEDED).setDuration(stepEndTime - stepStartTime)).incrementStepsExecuted().setVerdict({ status: FlowRunStatus.RUNNING })
}))
if (executionStateError) {
const failedStepOutput = stepOutput
.setStatus(StepOutputStatus.FAILED)
.setErrorMessage(utils.formatError(executionStateError))
.setDuration(performance.now() - stepStartTime)
return executionState
.upsertStep(action.name, failedStepOutput)
.setVerdict({
status: FlowRunStatus.FAILED, failedStep: {
name: action.name,
displayName: action.displayName,
message: utils.formatError(executionStateError),
},
})
}
return executionStateResult
}
function getResponse(hookResponse: HookResponse): RespondResponse | undefined {
switch (hookResponse.type) {
case 'stopped':
case 'respond':
return hookResponse.response.response
case 'paused':
if (hookResponse.response.pauseMetadata.type === PauseType.WEBHOOK) {
return hookResponse.response.pauseMetadata.response
}
else {
return undefined
}
case 'none':
return undefined
}
}
const createTagsManager = (hkParams: createTagsManagerParams): TagsManager => {
return {
add: async (params: addTagsParams): Promise<void> => {
hkParams.hookResponse.tags.push(params.name)
},
}
}
type addTagsParams = {
name: string
}
type createTagsManagerParams = {
hookResponse: HookResponse
}
function createStopHook(params: CreateStopHookParams): StopHook {
return (req?: StopHookParams) => {
params.hookResponse = {
...params.hookResponse,
type: 'stopped',
response: req ?? { response: {} },
}
}
}
type CreateStopHookParams = {
hookResponse: HookResponse
}
function createRespondHook(params: CreateRespondHookParams): RespondHook {
return (req?: RespondHookParams) => {
params.hookResponse = {
...params.hookResponse,
type: 'respond',
response: req ?? { response: {} },
}
}
}
type CreateRespondHookParams = {
hookResponse: HookResponse
}
function createPauseHook(params: CreatePauseHookParams, pauseId: string, requestIdToReply: string | null): PauseHook {
return (req) => {
switch (req.pauseMetadata.type) {
case PauseType.DELAY: {
const diffInDays = dayjs(req.pauseMetadata.resumeDateTime).diff(dayjs(), 'days')
if (diffInDays > AP_PAUSED_FLOW_TIMEOUT_DAYS) {
throw new PausedFlowTimeoutError(undefined, AP_PAUSED_FLOW_TIMEOUT_DAYS)
}
params.hookResponse = {
...params.hookResponse,
type: 'paused',
response: {
pauseMetadata: {
...req.pauseMetadata,
requestIdToReply: requestIdToReply ?? undefined,
},
},
}
break
}
case PauseType.WEBHOOK:
params.hookResponse = {
...params.hookResponse,
type: 'paused',
response: {
pauseMetadata: {
...req.pauseMetadata,
requestId: pauseId,
requestIdToReply: requestIdToReply ?? undefined,
response: req.pauseMetadata.response ?? {},
},
},
}
break
}
}
}
type CreatePauseHookParams = {
hookResponse: HookResponse
}

View File

@@ -0,0 +1,284 @@
import { LATEST_CONTEXT_VERSION } from '@activepieces/pieces-framework'
import { BranchCondition, BranchExecutionType, BranchOperator, EngineGenericError, FlowRunStatus, isNil, RouterAction, RouterActionSettings, RouterExecutionType, RouterStepOutput, StepOutputStatus } from '@activepieces/shared'
import dayjs from 'dayjs'
import { utils } from '../utils'
import { BaseExecutor } from './base-executor'
import { EngineConstants } from './context/engine-constants'
import { FlowExecutorContext } from './context/flow-execution-context'
import { flowExecutor } from './flow-executor'
export const routerExecuter: BaseExecutor<RouterAction> = {
async handle({
action,
executionState,
constants,
}) {
const { censoredInput, resolvedInput } = await constants.getPropsResolver(LATEST_CONTEXT_VERSION).resolve<RouterActionSettings>({
unresolvedInput: {
...action.settings,
},
executionState,
})
switch (resolvedInput.executionType) {
case RouterExecutionType.EXECUTE_ALL_MATCH:
return handleRouterExecution({ action, executionState, constants, censoredInput, resolvedInput, routerExecutionType: RouterExecutionType.EXECUTE_ALL_MATCH })
case RouterExecutionType.EXECUTE_FIRST_MATCH:
return handleRouterExecution({ action, executionState, constants, censoredInput, resolvedInput, routerExecutionType: RouterExecutionType.EXECUTE_FIRST_MATCH })
default:
throw new EngineGenericError('RouterExecutionTypeNotSupportedError', `Router execution type ${resolvedInput.executionType} is not supported`)
}
},
}
async function handleRouterExecution({ action, executionState, constants, censoredInput, resolvedInput, routerExecutionType }: {
action: RouterAction
executionState: FlowExecutorContext
constants: EngineConstants
censoredInput: unknown
resolvedInput: RouterActionSettings
routerExecutionType: RouterExecutionType
}): Promise<FlowExecutorContext> {
const stepStartTime = performance.now()
const evaluatedConditionsWithoutFallback = resolvedInput.branches.map((branch) => {
return branch.branchType === BranchExecutionType.FALLBACK ? true : evaluateConditions(branch.conditions)
})
const evaluatedConditions = resolvedInput.branches.map((branch, index) => {
if (branch.branchType === BranchExecutionType.CONDITION) {
return evaluatedConditionsWithoutFallback[index]
}
const fallback = evaluatedConditionsWithoutFallback.filter((_, i) => i !== index).every((condition) => !condition)
return fallback
})
const stepEndTime = performance.now()
const routerOutput = RouterStepOutput.init({
input: censoredInput,
}).setOutput({
branches: resolvedInput.branches.map((branch, index) => ({
branchName: branch.branchName,
branchIndex: index + 1,
evaluation: evaluatedConditions[index],
})),
}).setDuration(stepEndTime - stepStartTime)
executionState = executionState.upsertStep(action.name, routerOutput)
const { data: executionStateResult, error: executionStateError } = await utils.tryCatchAndThrowOnEngineError(async () => {
for (let i = 0; i < resolvedInput.branches.length; i++) {
if (!isNil(constants.stepNameToTest)) {
break
}
const condition = routerOutput.output?.branches[i].evaluation
if (!condition) {
continue
}
executionState = await flowExecutor.execute({
action: action.children[i],
executionState,
constants,
})
const shouldBreakExecution = executionState.verdict.status !== FlowRunStatus.RUNNING || routerExecutionType === RouterExecutionType.EXECUTE_FIRST_MATCH
if (shouldBreakExecution) {
break
}
}
return executionState
})
if (executionStateError) {
const failedStepOutput = routerOutput.setStatus(StepOutputStatus.FAILED)
return executionState.upsertStep(action.name, failedStepOutput).setVerdict({ status: FlowRunStatus.FAILED, failedStep: {
name: action.name,
displayName: action.displayName,
message: utils.formatError(executionStateError),
} })
}
return executionStateResult
}
export function evaluateConditions(conditionGroups: BranchCondition[][]): boolean {
let orOperator = false
for (const conditionGroup of conditionGroups) {
let andGroup = true
for (const condition of conditionGroup) {
const castedCondition = condition
if (isNil(castedCondition.operator)) {
throw new EngineGenericError('OperatorNotSetError', 'The operator is required but found to be undefined')
}
switch (castedCondition.operator) {
case BranchOperator.TEXT_CONTAINS: {
const firstValueContains = toLowercaseIfCaseInsensitive(castedCondition.firstValue, castedCondition.caseSensitive).includes(
toLowercaseIfCaseInsensitive(castedCondition.secondValue, castedCondition.caseSensitive),
)
andGroup = andGroup && firstValueContains
break
}
case BranchOperator.TEXT_DOES_NOT_CONTAIN: {
const firstValueDoesNotContain = !toLowercaseIfCaseInsensitive(castedCondition.firstValue, castedCondition.caseSensitive).includes(
toLowercaseIfCaseInsensitive(castedCondition.secondValue, castedCondition.caseSensitive),
)
andGroup = andGroup && firstValueDoesNotContain
break
}
case BranchOperator.TEXT_EXACTLY_MATCHES: {
const firstValueExactlyMatches = toLowercaseIfCaseInsensitive(castedCondition.firstValue, castedCondition.caseSensitive) ===
toLowercaseIfCaseInsensitive(castedCondition.secondValue, castedCondition.caseSensitive)
andGroup = andGroup && firstValueExactlyMatches
break
}
case BranchOperator.TEXT_DOES_NOT_EXACTLY_MATCH: {
const firstValueDoesNotExactlyMatch = toLowercaseIfCaseInsensitive(castedCondition.firstValue, castedCondition.caseSensitive) !==
toLowercaseIfCaseInsensitive(castedCondition.secondValue, castedCondition.caseSensitive)
andGroup = andGroup && firstValueDoesNotExactlyMatch
break
}
case BranchOperator.TEXT_STARTS_WITH: {
const firstValueStartsWith = toLowercaseIfCaseInsensitive(castedCondition.firstValue, castedCondition.caseSensitive).startsWith(
toLowercaseIfCaseInsensitive(castedCondition.secondValue, castedCondition.caseSensitive),
)
andGroup = andGroup && firstValueStartsWith
break
}
case BranchOperator.TEXT_ENDS_WITH: {
const firstValueEndsWith = toLowercaseIfCaseInsensitive(castedCondition.firstValue, castedCondition.caseSensitive).endsWith(
toLowercaseIfCaseInsensitive(castedCondition.secondValue, castedCondition.caseSensitive),
)
andGroup = andGroup && firstValueEndsWith
break
}
case BranchOperator.TEXT_DOES_NOT_START_WITH: {
const firstValueDoesNotStartWith = !toLowercaseIfCaseInsensitive(castedCondition.firstValue, castedCondition.caseSensitive).startsWith(
toLowercaseIfCaseInsensitive(castedCondition.secondValue, castedCondition.caseSensitive),
)
andGroup = andGroup && firstValueDoesNotStartWith
break
}
case BranchOperator.TEXT_DOES_NOT_END_WITH: {
const firstValueDoesNotEndWith = !toLowercaseIfCaseInsensitive(castedCondition.firstValue, castedCondition.caseSensitive).endsWith(
toLowercaseIfCaseInsensitive(castedCondition.secondValue, castedCondition.caseSensitive),
)
andGroup = andGroup && firstValueDoesNotEndWith
break
}
case BranchOperator.LIST_CONTAINS: {
const list = parseAndCoerceListAsArray(castedCondition.firstValue)
andGroup = andGroup && list.some((item) =>
toLowercaseIfCaseInsensitive(item, castedCondition.caseSensitive) === toLowercaseIfCaseInsensitive(castedCondition.secondValue, castedCondition.caseSensitive),
)
break
}
case BranchOperator.LIST_DOES_NOT_CONTAIN: {
const list = parseAndCoerceListAsArray(castedCondition.firstValue)
andGroup = andGroup && !list.some((item) =>
toLowercaseIfCaseInsensitive(item, castedCondition.caseSensitive) === toLowercaseIfCaseInsensitive(castedCondition.secondValue, castedCondition.caseSensitive),
)
break
}
case BranchOperator.NUMBER_IS_GREATER_THAN: {
const firstValue = parseStringToNumber(castedCondition.firstValue)
const secondValue = parseStringToNumber(castedCondition.secondValue)
andGroup = andGroup && firstValue > secondValue
break
}
case BranchOperator.NUMBER_IS_LESS_THAN: {
const firstValue = parseStringToNumber(castedCondition.firstValue)
const secondValue = parseStringToNumber(castedCondition.secondValue)
andGroup = andGroup && firstValue < secondValue
break
}
case BranchOperator.NUMBER_IS_EQUAL_TO: {
const firstValue = parseStringToNumber(castedCondition.firstValue)
const secondValue = parseStringToNumber(castedCondition.secondValue)
andGroup = andGroup && firstValue == secondValue
break
}
case BranchOperator.BOOLEAN_IS_TRUE:
andGroup = andGroup && !!castedCondition.firstValue
break
case BranchOperator.BOOLEAN_IS_FALSE:
andGroup = andGroup && !castedCondition.firstValue
break
case BranchOperator.DATE_IS_AFTER:
andGroup = andGroup && isValidDate(castedCondition.firstValue) && isValidDate(castedCondition.secondValue) && dayjs(castedCondition.firstValue).isAfter(dayjs(castedCondition.secondValue))
break
case BranchOperator.DATE_IS_EQUAL:
andGroup = andGroup && isValidDate(castedCondition.firstValue) && isValidDate(castedCondition.secondValue) && dayjs(castedCondition.firstValue).isSame(dayjs(castedCondition.secondValue))
break
case BranchOperator.DATE_IS_BEFORE:
andGroup = andGroup && isValidDate(castedCondition.firstValue) && isValidDate(castedCondition.secondValue) && dayjs(castedCondition.firstValue).isBefore(dayjs(castedCondition.secondValue))
break
case BranchOperator.LIST_IS_EMPTY: {
const list = parseListAsArray(castedCondition.firstValue)
andGroup = andGroup && Array.isArray(list) && list?.length === 0
break
}
case BranchOperator.LIST_IS_NOT_EMPTY: {
const list = parseListAsArray(castedCondition.firstValue)
andGroup = andGroup && Array.isArray(list) && list?.length !== 0
break
}
case BranchOperator.EXISTS:
andGroup = andGroup && castedCondition.firstValue !== undefined && castedCondition.firstValue !== null && castedCondition.firstValue !== ''
break
case BranchOperator.DOES_NOT_EXIST:
andGroup = andGroup && (castedCondition.firstValue === undefined || castedCondition.firstValue === null || castedCondition.firstValue === '')
break
}
}
orOperator = orOperator || andGroup
}
return Boolean(orOperator)
}
function toLowercaseIfCaseInsensitive(text: unknown, caseSensitive: boolean | undefined): string {
if (typeof text === 'string') {
return caseSensitive ? text : text.toLowerCase()
}
const textAsString = JSON.stringify(text)
return caseSensitive ? textAsString : textAsString.toLowerCase()
}
function parseStringToNumber(str: string): number | string {
const num = Number(str)
return isNaN(num) ? str : num
}
function parseListAsArray(input: unknown): unknown[] | undefined {
if (typeof input === 'string') {
try {
const parsed = JSON.parse(input)
return Array.isArray(parsed) ? parsed : undefined
}
catch (e) {
return undefined
}
}
return Array.isArray(input) ? input : undefined
}
function parseAndCoerceListAsArray(input: unknown): unknown[] {
if (typeof input === 'string') {
try {
const parsed = JSON.parse(input)
return Array.isArray(parsed) ? parsed : [parsed]
}
catch (e) {
return [input]
}
}
return Array.isArray(input) ? input : [input]
}
function isValidDate(date: unknown): boolean {
if (typeof date === 'string' || typeof date === 'number' || date instanceof Date) {
return dayjs(date).isValid()
}
return false
}

View File

@@ -0,0 +1,59 @@
import { CodeAction, FlowRunStatus, isNil, PieceAction } from '@activepieces/shared'
import { EngineConstants } from '../handler/context/engine-constants'
import { FlowExecutorContext } from '../handler/context/flow-execution-context'
export async function runWithExponentialBackoff<T extends CodeAction | PieceAction>(
executionState: FlowExecutorContext,
action: T,
constants: EngineConstants,
requestFunction: RequestFunction<T>,
attemptCount = 1,
): Promise<FlowExecutorContext> {
const resultExecutionState = await requestFunction({ action, executionState, constants })
const retryEnabled = action.settings.errorHandlingOptions?.retryOnFailure?.value
if (
executionFailedWithRetryableError(resultExecutionState) &&
attemptCount < constants.retryConstants.maxAttempts &&
retryEnabled &&
isNil(constants.stepNameToTest)
) {
const backoffTime = Math.pow(constants.retryConstants.retryExponential, attemptCount) * constants.retryConstants.retryInterval
await new Promise(resolve => setTimeout(resolve, backoffTime))
return runWithExponentialBackoff(executionState, action, constants, requestFunction, attemptCount + 1)
}
return resultExecutionState
}
export async function continueIfFailureHandler(
executionState: FlowExecutorContext,
action: CodeAction | PieceAction,
constants: EngineConstants,
): Promise<FlowExecutorContext> {
const continueOnFailure = action.settings.errorHandlingOptions?.continueOnFailure?.value
if (
executionState.verdict.status === FlowRunStatus.FAILED &&
continueOnFailure &&
isNil(constants.stepNameToTest)
) {
return executionState
.setVerdict({ status: FlowRunStatus.RUNNING })
}
return executionState
}
const executionFailedWithRetryableError = (flowExecutorContext: FlowExecutorContext): boolean => {
return flowExecutorContext.verdict.status === FlowRunStatus.FAILED
}
type Request<T extends CodeAction | PieceAction> = {
action: T
executionState: FlowExecutorContext
constants: EngineConstants
}
type RequestFunction<T extends CodeAction | PieceAction> = (request: Request<T>) => Promise<FlowExecutorContext>

View File

@@ -0,0 +1,128 @@
import { isObject, StepOutput } from '@activepieces/shared'
import { Queue } from '@datastructures-js/queue'
import sizeof from 'object-sizeof'
import PriorityQueue from 'priority-queue-typescript'
const TRUNCATION_TEXT_PLACEHOLDER = '(truncated)'
const ERROR_OFFSET = 256 * 1024
const DEFAULT_MAX_LOG_SIZE_FOR_TESTING = '10'
const MAX_LOG_SIZE = Number(process.env.AP_MAX_FILE_SIZE_MB ?? DEFAULT_MAX_LOG_SIZE_FOR_TESTING) * 1024 * 1024
const MAX_SIZE_FOR_ALL_ENTRIES = MAX_LOG_SIZE - ERROR_OFFSET
const SIZE_OF_TRUNCATION_TEXT_PLACEHOLDER = sizeof(TRUNCATION_TEXT_PLACEHOLDER)
const nonTruncatableKeys: Key[] = ['status', 'duration', 'type']
export const loggingUtils = {
async trimExecution(steps: Record<string, StepOutput>): Promise<Record<string, StepOutput>> {
const totalJsonSize = sizeof(steps)
if (!jsonExceedMaxSize(totalJsonSize)) {
return steps
}
return removeLeavesInTopologicalOrder(JSON.parse(JSON.stringify(steps)))
},
}
function removeLeavesInTopologicalOrder(json: Record<string, unknown>): Record<string, StepOutput> {
const nodes: Node[] = traverseJsonAndConvertToNodes(json)
const leaves = new PriorityQueue<Node>(
undefined,
(a: Node, b: Node) => b.size - a.size,
)
nodes.filter((node) => node.numberOfChildren === 0).forEach((node) => leaves.add(node))
let totalJsonSize = sizeof(json)
while (!leaves.empty() && jsonExceedMaxSize(totalJsonSize)) {
const curNode = leaves.poll()
const isDepthGreaterThanOne = curNode && curNode.depth > 1
const isTruncatable = curNode && (!nonTruncatableKeys.includes(curNode.key))
if (isDepthGreaterThanOne && isTruncatable) {
totalJsonSize += SIZE_OF_TRUNCATION_TEXT_PLACEHOLDER - curNode.size
const parent = curNode.parent
parent.value[curNode.key] = TRUNCATION_TEXT_PLACEHOLDER
nodes[parent.index].numberOfChildren--
if (nodes[parent.index].numberOfChildren == 0) {
leaves.add(nodes[parent.index])
}
}
}
return json as Record<string, StepOutput>
}
function traverseJsonAndConvertToNodes(root: unknown) {
const nodesQueue = new Queue<BfsNode>()
nodesQueue.enqueue({ key: '', value: root, parent: { index: -1, value: {} }, depth: 0 })
const nodes: Node[] = []
while (!nodesQueue.isEmpty()) {
const curNode = nodesQueue.dequeue()
const children = findChildren(curNode.value, curNode.key === 'iterations')
nodes.push({
index: nodes.length,
size: children.length === 0 ? sizeof(curNode.value) : children.length * SIZE_OF_TRUNCATION_TEXT_PLACEHOLDER,
key: curNode.key,
parent: {
index: curNode.parent.index,
value: curNode.parent.value as Record<Key, unknown>,
},
numberOfChildren: children.length,
depth: curNode.depth,
})
children.forEach((child) => {
const key = child[0], value = child[1]
nodesQueue.enqueue({ value, key, parent: { index: nodes.length - 1, value: curNode.value }, depth: curNode.depth + 1 })
})
}
return nodes
}
function findChildren(curNode: unknown, traverseArray: boolean): [Key, unknown][] {
if (isObject(curNode)) {
return Object.entries(curNode)
}
// Array should be treated as a leaf node as If it has too many small items, It will prioritize the other steps first
if (Array.isArray(curNode) && traverseArray) {
const children: [Key, unknown][] = []
for (let i = 0; i < curNode.length; i++) {
children.push([i, curNode[i]])
}
return children
}
return []
}
const jsonExceedMaxSize = (jsonSize: number): boolean => {
return jsonSize > MAX_SIZE_FOR_ALL_ENTRIES
}
type Node = {
index: number
size: number
key: Key
parent: {
index: number
value: Record<Key, unknown>
}
numberOfChildren: number
depth: number
}
type BfsNode = {
value: unknown
key: Key
parent: {
index: number
value: unknown
}
depth: number
}
type Key = string | number | symbol

View File

@@ -0,0 +1,253 @@
import {
DropdownProperty,
DynamicProperties,
ExecutePropsResult,
getAuthPropertyForValue,
MultiSelectDropdownProperty,
PieceAuthProperty,
PieceMetadata,
PiecePropertyMap,
pieceTranslation,
PropertyType,
StaticPropsValue } from '@activepieces/pieces-framework'
import {
AppConnectionType,
AppConnectionValue,
EngineGenericError,
ExecuteExtractPieceMetadata,
ExecutePropsOptions,
ExecuteValidateAuthOperation,
ExecuteValidateAuthResponse,
isNil,
} from '@activepieces/shared'
import { EngineConstants } from '../handler/context/engine-constants'
import { testExecutionContext } from '../handler/context/test-execution-context'
import { createFlowsContext } from '../services/flows.service'
import { utils } from '../utils'
import { createPropsResolver } from '../variables/props-resolver'
import { pieceLoader } from './piece-loader'
export const pieceHelper = {
async executeProps( operation: ExecutePropsParams): Promise<ExecutePropsResult<PropertyType.DROPDOWN | PropertyType.MULTI_SELECT_DROPDOWN | PropertyType.DYNAMIC>> {
const constants = EngineConstants.fromExecutePropertyInput(operation)
const executionState = await testExecutionContext.stateFromFlowVersion({
apiUrl: operation.internalApiUrl,
flowVersion: operation.flowVersion,
projectId: operation.projectId,
engineToken: operation.engineToken,
sampleData: operation.sampleData,
})
const { property, piece } = await pieceLoader.getPropOrThrow({ pieceName: operation.pieceName, pieceVersion: operation.pieceVersion, actionOrTriggerName: operation.actionOrTriggerName, propertyName: operation.propertyName, devPieces: EngineConstants.DEV_PIECES })
if (property.type !== PropertyType.DROPDOWN && property.type !== PropertyType.MULTI_SELECT_DROPDOWN && property.type !== PropertyType.DYNAMIC) {
throw new EngineGenericError('PropertyTypeNotExecutableError', `Property type is not executable: ${property.type} for ${property.displayName}`)
}
const { data: executePropsResult, error: executePropsError } = await utils.tryCatchAndThrowOnEngineError((async (): Promise<ExecutePropsResult<PropertyType.DROPDOWN | PropertyType.MULTI_SELECT_DROPDOWN | PropertyType.DYNAMIC>> => {
const { resolvedInput } = await createPropsResolver({
apiUrl: constants.internalApiUrl,
projectId: constants.projectId,
engineToken: constants.engineToken,
contextVersion: piece.getContextInfo?.().version,
}).resolve<
StaticPropsValue<PiecePropertyMap>
>({
unresolvedInput: operation.input,
executionState,
})
const ctx = {
searchValue: operation.searchValue,
server: {
token: constants.engineToken,
apiUrl: constants.internalApiUrl,
publicUrl: operation.publicApiUrl,
},
project: {
id: constants.projectId,
externalId: constants.externalProjectId,
},
flows: createFlowsContext(constants),
step: {
name: operation.actionOrTriggerName,
},
connections: utils.createConnectionManager({
projectId: constants.projectId,
engineToken: constants.engineToken,
apiUrl: constants.internalApiUrl,
target: 'properties',
contextVersion: piece.getContextInfo?.().version,
}),
}
switch (property.type) {
case PropertyType.DYNAMIC: {
const dynamicProperty = property as DynamicProperties<boolean>
const props = await dynamicProperty.props(resolvedInput, ctx)
return {
type: PropertyType.DYNAMIC,
options: props,
}
}
case PropertyType.MULTI_SELECT_DROPDOWN: {
const multiSelectProperty = property as MultiSelectDropdownProperty<
unknown,
boolean
>
const options = await multiSelectProperty.options(resolvedInput, ctx)
return {
type: PropertyType.MULTI_SELECT_DROPDOWN,
options,
}
}
case PropertyType.DROPDOWN: {
const dropdownProperty = property as DropdownProperty<unknown, boolean>
const options = await dropdownProperty.options(resolvedInput, ctx)
return {
type: PropertyType.DROPDOWN,
options,
}
}
default: {
throw new EngineGenericError('PropertyTypeNotExecutableError', `Property type is not executable: ${property}`)
}
}
}))
if (executePropsError) {
console.error(executePropsError)
return {
type: property.type,
options: {
disabled: true,
options: [],
placeholder: 'Throws an error, reconnect or refresh the page',
},
}
}
return executePropsResult
},
async executeValidateAuth(
{ params, devPieces }: { params: ExecuteValidateAuthOperation, devPieces: string[] },
): Promise<ExecuteValidateAuthResponse> {
const { piece: piecePackage } = params
const piece = await pieceLoader.loadPieceOrThrow({ pieceName: piecePackage.pieceName, pieceVersion: piecePackage.pieceVersion, devPieces })
const server = {
apiUrl: params.internalApiUrl.endsWith('/') ? params.internalApiUrl : params.internalApiUrl + '/',
publicUrl: params.publicApiUrl,
}
return validateAuth({
authValue: params.auth,
pieceAuth: piece.auth,
server,
})
},
async extractPieceMetadata({ devPieces, params }: { devPieces: string[], params: ExecuteExtractPieceMetadata }): Promise<PieceMetadata> {
const { pieceName, pieceVersion } = params
const piece = await pieceLoader.loadPieceOrThrow({ pieceName, pieceVersion, devPieces })
const pieceAlias = pieceLoader.getPackageAlias({ pieceName, pieceVersion, devPieces })
const pieceFolderPath = await pieceLoader.getPiecePath({ packageName: pieceAlias, devPieces })
const i18n = await pieceTranslation.initializeI18n(pieceFolderPath)
const fullMetadata = piece.metadata()
return {
...fullMetadata,
name: pieceName,
version: pieceVersion,
authors: piece.authors,
i18n,
}
},
}
type ExecutePropsParams = Omit<ExecutePropsOptions, 'piece'> & { pieceName: string, pieceVersion: string }
function mismatchAuthTypeErrorMessage(pieceAuthType: PropertyType, connectionType: AppConnectionType): ExecuteValidateAuthResponse {
return {
valid: false,
error: `Connection value type does not match piece auth type: ${pieceAuthType} !== ${connectionType}`,
}
}
const validateAuth = async ({
server,
authValue,
pieceAuth,
}: ValidateAuthParams): Promise<ExecuteValidateAuthResponse> => {
if (isNil(pieceAuth)) {
return {
valid: true,
}
}
const usedPieceAuth = getAuthPropertyForValue({
authValueType: authValue.type,
pieceAuth,
})
if (isNil(usedPieceAuth)) {
return {
valid: false,
error: 'No piece auth found for auth value',
}
}
if (isNil(usedPieceAuth.validate)) {
return {
valid: true,
}
}
switch (usedPieceAuth.type) {
case PropertyType.OAUTH2:{
if (authValue.type !== AppConnectionType.OAUTH2 && authValue.type !== AppConnectionType.CLOUD_OAUTH2 && authValue.type !== AppConnectionType.PLATFORM_OAUTH2) {
return mismatchAuthTypeErrorMessage(usedPieceAuth.type, authValue.type)
}
return usedPieceAuth.validate({
auth: authValue,
server,
})
}
case PropertyType.BASIC_AUTH:{
if (authValue.type !== AppConnectionType.BASIC_AUTH) {
return mismatchAuthTypeErrorMessage(usedPieceAuth.type, authValue.type)
}
return usedPieceAuth.validate({
auth: authValue,
server,
})
}
case PropertyType.SECRET_TEXT:{
if (authValue.type !== AppConnectionType.SECRET_TEXT) {
return mismatchAuthTypeErrorMessage(usedPieceAuth.type, authValue.type)
}
return usedPieceAuth.validate({
auth: authValue.secret_text,
server,
})
}
case PropertyType.CUSTOM_AUTH:{
if (authValue.type !== AppConnectionType.CUSTOM_AUTH) {
return mismatchAuthTypeErrorMessage(usedPieceAuth.type, authValue.type)
}
return usedPieceAuth.validate({
auth: authValue.props,
server,
})
}
default: {
throw new EngineGenericError('InvalidAuthTypeError', 'Invalid auth type')
}
}
}
type ValidateAuthParams = {
server: {
apiUrl: string
publicUrl: string
}
authValue: AppConnectionValue
pieceAuth: PieceAuthProperty | PieceAuthProperty[] | undefined
}

View File

@@ -0,0 +1,208 @@
import fs from 'fs/promises'
import path from 'path'
import { Action, Piece, PiecePropertyMap, Trigger } from '@activepieces/pieces-framework'
import { ActivepiecesError, EngineGenericError, ErrorCode, extractPieceFromModule, getPackageAliasForPiece, getPieceNameFromAlias, isNil, trimVersionFromAlias } from '@activepieces/shared'
import { utils } from '../utils'
export const pieceLoader = {
loadPieceOrThrow: async (
{ pieceName, pieceVersion, devPieces }: LoadPieceParams,
): Promise<Piece> => {
const { data: piece, error: pieceError } = await utils.tryCatchAndThrowOnEngineError(async () => {
const packageName = pieceLoader.getPackageAlias({
pieceName,
pieceVersion,
devPieces,
})
const piecePath = await pieceLoader.getPiecePath({ packageName, devPieces })
const module = await import(piecePath)
const piece = extractPieceFromModule<Piece>({
module,
pieceName,
pieceVersion,
})
if (isNil(piece)) {
throw new EngineGenericError('PieceNotFoundError', `Piece not found for piece: ${pieceName}, pieceVersion: ${pieceVersion}`)
}
return piece
})
if (pieceError) {
throw pieceError
}
return piece
},
getPieceAndTriggerOrThrow: async (params: GetPieceAndTriggerParams): Promise<{ piece: Piece, pieceTrigger: Trigger }> => {
const { pieceName, pieceVersion, triggerName, devPieces } = params
const piece = await pieceLoader.loadPieceOrThrow({ pieceName, pieceVersion, devPieces })
const trigger = piece.getTrigger(triggerName)
if (trigger === undefined) {
throw new EngineGenericError('TriggerNotFoundError', `Trigger not found, pieceName=${pieceName}, triggerName=${triggerName}`)
}
return {
piece,
pieceTrigger: trigger,
}
},
getPieceAndActionOrThrow: async (params: GetPieceAndActionParams): Promise<{ piece: Piece, pieceAction: Action }> => {
const { pieceName, pieceVersion, actionName, devPieces } = params
const piece = await pieceLoader.loadPieceOrThrow({ pieceName, pieceVersion, devPieces })
const pieceAction = piece.getAction(actionName)
if (isNil(pieceAction)) {
throw new ActivepiecesError({
code: ErrorCode.STEP_NOT_FOUND,
params: {
pieceName,
pieceVersion,
stepName: actionName,
},
})
}
return {
piece,
pieceAction,
}
},
getPropOrThrow: async ({ pieceName, pieceVersion, actionOrTriggerName, propertyName, devPieces }: GetPropParams) => {
const piece = await pieceLoader.loadPieceOrThrow({ pieceName, pieceVersion, devPieces })
const actionOrTrigger = piece.getAction(actionOrTriggerName) ?? piece.getTrigger(actionOrTriggerName)
if (isNil(actionOrTrigger)) {
throw new ActivepiecesError({
code: ErrorCode.STEP_NOT_FOUND,
params: {
pieceName,
pieceVersion,
stepName: actionOrTriggerName,
},
})
}
const property = (actionOrTrigger.props as PiecePropertyMap)[propertyName]
if (isNil(property)) {
throw new ActivepiecesError({
code: ErrorCode.CONFIG_NOT_FOUND,
params: {
pieceName,
pieceVersion,
stepName: actionOrTriggerName,
configName: propertyName,
},
})
}
return { property, piece }
},
getPackageAlias: ({ pieceName, pieceVersion, devPieces }: GetPackageAliasParams) => {
if (devPieces.includes(getPieceNameFromAlias(pieceName))) {
return pieceName
}
return getPackageAliasForPiece({
pieceName,
pieceVersion,
})
},
getPiecePath: async ({ packageName, devPieces }: GetPiecePathParams): Promise<string> => {
const piecePath = devPieces.includes(getPieceNameFromAlias(packageName))
? await loadPieceFromDistFolder(packageName)
: await traverseAllParentFoldersToFindPiece(packageName)
if (isNil(piecePath)) {
throw new EngineGenericError('PieceNotFoundError', `Piece not found for package: ${packageName}`)
}
return piecePath
},
}
async function loadPieceFromDistFolder(packageName: string): Promise<string | null> {
const distPath = path.resolve('dist/packages/pieces')
const entries = (await utils.walk(distPath)).filter((entry) => entry.name === 'package.json')
for (const entry of entries) {
const { data: packageJsonPath } = await utils.tryCatchAndThrowOnEngineError((async () => {
const packageJsonPath = entry.path
const packageJsonContent = await fs.readFile(packageJsonPath, 'utf-8')
const packageJson = JSON.parse(packageJsonContent)
if (packageJson.name === packageName) {
return path.dirname(packageJsonPath)
}
return null
}))
if (packageJsonPath) {
return packageJsonPath
}
}
return null
}
async function traverseAllParentFoldersToFindPiece(packageName: string): Promise<string | null> {
const rootDir = path.parse(__dirname).root
let currentDir = __dirname
const maxIterations = currentDir.split(path.sep).length
for (let i = 0; i < maxIterations; i++) {
const piecePath = path.resolve(currentDir, 'pieces', packageName, 'node_modules', trimVersionFromAlias(packageName))
if (await utils.folderExists(piecePath)) {
return piecePath
}
const parentDir = path.dirname(currentDir)
if (parentDir === currentDir || currentDir === rootDir) {
break
}
currentDir = parentDir
}
return null
}
type GetPiecePathParams = {
packageName: string
devPieces: string[]
}
type LoadPieceParams = {
pieceName: string
pieceVersion: string
devPieces: string[]
}
type GetPieceAndTriggerParams = {
pieceName: string
pieceVersion: string
triggerName: string
devPieces: string[]
}
type GetPieceAndActionParams = {
pieceName: string
pieceVersion: string
actionName: string
devPieces: string[]
}
type GetPropParams = {
pieceName: string
pieceVersion: string
actionOrTriggerName: string
propertyName: string
devPieces: string[]
}
type GetPackageAliasParams = {
pieceName: string
devPieces: string[]
pieceVersion: string
}

View File

@@ -0,0 +1,312 @@
import { inspect } from 'node:util'
import { PiecePropertyMap, StaticPropsValue, TriggerStrategy } from '@activepieces/pieces-framework'
import { assertEqual, AUTHENTICATION_PROPERTY_NAME, EngineGenericError, EventPayload, ExecuteTriggerOperation, ExecuteTriggerResponse, FlowTrigger, InvalidCronExpressionError, isNil, PieceTrigger, PropertySettings, ScheduleOptions, TriggerHookType, TriggerSourceScheduleType } from '@activepieces/shared'
import { isValidCron } from 'cron-validator'
import { EngineConstants } from '../handler/context/engine-constants'
import { FlowExecutorContext } from '../handler/context/flow-execution-context'
import { createFlowsContext } from '../services/flows.service'
import { createFilesService } from '../services/step-files.service'
import { createContextStore } from '../services/storage.service'
import { utils } from '../utils'
import { propsProcessor } from '../variables/props-processor'
import { createPropsResolver } from '../variables/props-resolver'
import { pieceLoader } from './piece-loader'
type Listener = {
events: string[]
identifierValue: string
identifierKey: string
}
export const triggerHelper = {
async executeOnStart(trigger: FlowTrigger, constants: EngineConstants, payload: unknown) {
const { pieceName, pieceVersion, triggerName, input, propertySettings } = (trigger as PieceTrigger).settings
if (isNil(triggerName)) {
throw new EngineGenericError('TriggerNameNotSetError', 'Trigger name is not set')
}
const { pieceTrigger, processedInput, piece } = await prepareTriggerExecution({
pieceName,
pieceVersion,
triggerName,
input,
projectId: constants.projectId,
apiUrl: constants.internalApiUrl,
engineToken: constants.engineToken,
devPieces: constants.devPieces,
propertySettings,
})
const isOldVersionOrNotSupported = isNil(pieceTrigger.onStart)
if (isOldVersionOrNotSupported) {
return
}
const context = {
store: createContextStore({
apiUrl: constants.internalApiUrl,
prefix: '',
flowId: constants.flowId,
engineToken: constants.engineToken,
}),
auth: processedInput[AUTHENTICATION_PROPERTY_NAME],
propsValue: processedInput,
payload,
run: {
id: constants.flowRunId,
},
step: {
name: triggerName,
},
project: {
id: constants.projectId,
externalId: constants.externalProjectId,
},
connections: utils.createConnectionManager({
apiUrl: constants.internalApiUrl,
projectId: constants.projectId,
engineToken: constants.engineToken,
target: 'triggers',
contextVersion: piece.getContextInfo?.().version,
}),
}
await pieceTrigger.onStart(context)
},
async executeTrigger({ params, constants }: ExecuteTriggerParams): Promise<ExecuteTriggerResponse<TriggerHookType>> {
const { pieceName, pieceVersion, triggerName, input, propertySettings } = (params.flowVersion.trigger as PieceTrigger).settings
if (isNil(triggerName)) {
throw new EngineGenericError('TriggerNameNotSetError', 'Trigger name is not set')
}
const { piece, pieceTrigger, processedInput } = await prepareTriggerExecution({
pieceName,
pieceVersion,
triggerName,
input,
projectId: params.projectId,
apiUrl: constants.internalApiUrl,
engineToken: params.engineToken,
devPieces: constants.devPieces,
propertySettings,
})
const appListeners: Listener[] = []
const prefix = params.test ? 'test' : ''
let scheduleOptions: ScheduleOptions | undefined = undefined
const context = {
store: createContextStore({
apiUrl: constants.internalApiUrl,
prefix,
flowId: params.flowVersion.flowId,
engineToken: params.engineToken,
}),
step: {
name: triggerName,
},
app: {
createListeners({ events, identifierKey, identifierValue }: Listener): void {
appListeners.push({ events, identifierValue, identifierKey })
},
},
setSchedule(request: ScheduleOptions) {
if (!isValidCron(request.cronExpression)) {
throw new InvalidCronExpressionError(request.cronExpression)
}
scheduleOptions = {
type: TriggerSourceScheduleType.CRON_EXPRESSION,
cronExpression: request.cronExpression,
timezone: request.timezone ?? 'UTC',
}
},
flows: createFlowsContext({
engineToken: params.engineToken,
internalApiUrl: constants.internalApiUrl,
flowId: params.flowVersion.flowId,
flowVersionId: params.flowVersion.id,
}),
webhookUrl: params.webhookUrl,
auth: processedInput[AUTHENTICATION_PROPERTY_NAME],
propsValue: processedInput,
payload: params.triggerPayload ?? {},
project: {
id: params.projectId,
externalId: constants.externalProjectId,
},
server: {
token: params.engineToken,
apiUrl: constants.internalApiUrl,
publicUrl: params.publicApiUrl,
},
connections: utils.createConnectionManager({
apiUrl: constants.internalApiUrl,
projectId: constants.projectId,
engineToken: constants.engineToken,
target: 'triggers',
contextVersion: piece.getContextInfo?.().version,
}),
}
switch (params.hookType) {
case TriggerHookType.ON_DISABLE: {
await pieceTrigger.onDisable(context)
return {}
}
case TriggerHookType.ON_ENABLE: {
await pieceTrigger.onEnable(context)
return {
listeners: appListeners,
scheduleOptions: pieceTrigger.type === TriggerStrategy.POLLING ? scheduleOptions : undefined,
}
}
case TriggerHookType.RENEW: {
assertEqual(pieceTrigger.type, TriggerStrategy.WEBHOOK, 'triggerType', 'WEBHOOK')
await pieceTrigger.onRenew(context)
return {
success: true,
}
}
case TriggerHookType.HANDSHAKE: {
const { data: handshakeResponse, error: handshakeResponseError } = await utils.tryCatchAndThrowOnEngineError(() => pieceTrigger.onHandshake(context))
if (handshakeResponseError) {
console.error(handshakeResponseError)
return {
success: false,
message: `Error while testing trigger: ${inspect(handshakeResponseError)}`,
}
}
return {
success: true,
response: handshakeResponse,
}
}
case TriggerHookType.TEST: {
const { data: testResponse, error: testResponseError } = await utils.tryCatchAndThrowOnEngineError(() => pieceTrigger.test({
...context,
files: createFilesService({
apiUrl: constants.internalApiUrl,
engineToken: params.engineToken!,
stepName: triggerName,
flowId: params.flowVersion.flowId,
}),
}))
if (testResponseError) {
console.error(testResponseError)
return {
success: false,
message: `Error while testing trigger: ${inspect(testResponseError)}`,
output: [],
}
}
return {
success: true,
output: testResponse,
}
}
case TriggerHookType.RUN: {
if (pieceTrigger.type === TriggerStrategy.APP_WEBHOOK) {
const { data: verified, error: verifiedError } = await utils.tryCatchAndThrowOnEngineError(async () => {
if (!params.appWebhookUrl) {
throw new EngineGenericError('AppWebhookUrlNotAvailableError', `App webhook url is not available for piece name ${pieceName}`)
}
if (!params.webhookSecret) {
throw new EngineGenericError('WebhookSecretNotAvailableError', `Webhook secret is not available for piece name ${pieceName}`)
}
return piece.events?.verify({
appWebhookUrl: params.appWebhookUrl,
payload: params.triggerPayload as EventPayload,
webhookSecret: params.webhookSecret,
})
})
if (verifiedError) {
return {
success: false,
message: `Error while verifying webhook: ${inspect(verifiedError)}`,
output: [],
}
}
if (isNil(verified)) {
return {
success: false,
message: 'Webhook is not verified',
output: [],
}
}
}
const { data: triggerRunResult, error: triggerRunError } = await utils.tryCatchAndThrowOnEngineError(async () => {
const items = await pieceTrigger.run({
...context,
files: createFilesService({
apiUrl: constants.internalApiUrl,
engineToken: params.engineToken!,
flowId: params.flowVersion.flowId,
stepName: triggerName,
}),
})
return {
success: true,
output: items,
}
})
if (triggerRunError) {
return {
success: false,
message: triggerRunError.message,
output: [],
}
}
return triggerRunResult
}
}
},
}
type ExecuteTriggerParams = {
params: ExecuteTriggerOperation<TriggerHookType>
constants: EngineConstants
}
async function prepareTriggerExecution({ pieceName, pieceVersion, triggerName, input, propertySettings, projectId, apiUrl, engineToken, devPieces }: PrepareTriggerExecutionParams) {
const { piece, pieceTrigger } = await pieceLoader.getPieceAndTriggerOrThrow({
pieceName,
pieceVersion,
triggerName,
devPieces,
})
const { resolvedInput } = await createPropsResolver({
apiUrl,
projectId,
engineToken,
contextVersion: piece.getContextInfo?.().version,
}).resolve<StaticPropsValue<PiecePropertyMap>>({
unresolvedInput: input,
executionState: FlowExecutorContext.empty(),
})
const { processedInput, errors } = await propsProcessor.applyProcessorsAndValidators(resolvedInput, pieceTrigger.props, piece.auth, pieceTrigger.requireAuth, propertySettings)
if (Object.keys(errors).length > 0) {
throw new Error(JSON.stringify(errors, null, 2))
}
return { piece, pieceTrigger, processedInput }
}
type PrepareTriggerExecutionParams = {
pieceName: string
pieceVersion: string
triggerName: string
input: unknown
propertySettings: Record<string, PropertySettings>
projectId: string
apiUrl: string
engineToken: string
devPieces: string[]
}

View File

@@ -0,0 +1,23 @@
import {
EngineResponse,
EngineResponseStatus,
ExecuteValidateAuthOperation,
ExecuteValidateAuthResponse,
} from '@activepieces/shared'
import { EngineConstants } from '../handler/context/engine-constants'
import { pieceHelper } from '../helper/piece-helper'
export const authValidationOperation = {
execute: async (operation: ExecuteValidateAuthOperation): Promise<EngineResponse<ExecuteValidateAuthResponse>> => {
const input = operation as ExecuteValidateAuthOperation
const output = await pieceHelper.executeValidateAuth({
params: input,
devPieces: EngineConstants.DEV_PIECES,
})
return {
status: EngineResponseStatus.OK,
response: output,
}
},
}

View File

@@ -0,0 +1,135 @@
import {
BeginExecuteFlowOperation,
EngineResponse,
EngineResponseStatus,
ExecuteFlowOperation,
ExecuteTriggerResponse,
ExecutionType,
FlowActionType,
flowStructureUtil,
GenericStepOutput,
isNil,
LoopStepOutput,
StepOutput,
StepOutputStatus,
TriggerHookType,
TriggerPayload,
} from '@activepieces/shared'
import { EngineConstants } from '../handler/context/engine-constants'
import { FlowExecutorContext } from '../handler/context/flow-execution-context'
import { testExecutionContext } from '../handler/context/test-execution-context'
import { flowExecutor } from '../handler/flow-executor'
import { triggerHelper } from '../helper/trigger-helper'
import { progressService } from '../services/progress.service'
export const flowOperation = {
execute: async (operation: ExecuteFlowOperation): Promise<EngineResponse<undefined>> => {
const input = operation as ExecuteFlowOperation
const constants = EngineConstants.fromExecuteFlowInput(input)
const output: FlowExecutorContext = (await executieSingleStepOrFlowOperation(input)).finishExecution()
await progressService.sendUpdate({
engineConstants: constants,
flowExecutorContext: output,
updateImmediate: true,
})
return {
status: EngineResponseStatus.OK,
response: undefined,
delayInSeconds: output.getDelayedInSeconds(),
}
},
}
const executieSingleStepOrFlowOperation = async (input: ExecuteFlowOperation): Promise<FlowExecutorContext> => {
const constants = EngineConstants.fromExecuteFlowInput(input)
const testSingleStepMode = !isNil(constants.stepNameToTest)
if (testSingleStepMode) {
const testContext = await testExecutionContext.stateFromFlowVersion({
apiUrl: input.internalApiUrl,
flowVersion: input.flowVersion,
excludedStepName: input.stepNameToTest!,
projectId: input.projectId,
engineToken: input.engineToken,
sampleData: input.sampleData,
})
const step = flowStructureUtil.getActionOrThrow(input.stepNameToTest!, input.flowVersion.trigger)
return flowExecutor.execute({
action: step,
executionState: await getFlowExecutionState(input, testContext),
constants: EngineConstants.fromExecuteFlowInput(input),
})
}
return flowExecutor.executeFromTrigger({
executionState: await getFlowExecutionState(input, FlowExecutorContext.empty()),
constants,
input,
})
}
async function getFlowExecutionState(input: ExecuteFlowOperation, flowContext: FlowExecutorContext): Promise<FlowExecutorContext> {
switch (input.executionType) {
case ExecutionType.BEGIN: {
const newPayload = await runOrReturnPayload(input)
flowContext = flowContext.upsertStep(input.flowVersion.trigger.name, GenericStepOutput.create({
type: input.flowVersion.trigger.type,
status: StepOutputStatus.SUCCEEDED,
input: {},
}).setOutput(newPayload))
break
}
case ExecutionType.RESUME: {
break
}
}
for (const [step, output] of Object.entries(input.executionState.steps)) {
if ([StepOutputStatus.SUCCEEDED, StepOutputStatus.PAUSED].includes(output.status)) {
const newOutput = await insertSuccessStepsOrPausedRecursively(output)
if (!isNil(newOutput)) {
flowContext = flowContext.upsertStep(step, newOutput)
}
}
}
return flowContext
}
async function runOrReturnPayload(input: BeginExecuteFlowOperation): Promise<TriggerPayload> {
if (!input.executeTrigger) {
return input.triggerPayload as TriggerPayload
}
const newPayload = await triggerHelper.executeTrigger({
params: {
...input,
hookType: TriggerHookType.RUN,
test: false,
webhookUrl: '',
triggerPayload: input.triggerPayload as TriggerPayload,
},
constants: EngineConstants.fromExecuteFlowInput(input),
}) as ExecuteTriggerResponse<TriggerHookType.RUN>
return newPayload.output[0] as TriggerPayload
}
async function insertSuccessStepsOrPausedRecursively(stepOutput: StepOutput): Promise<StepOutput | null> {
if (![StepOutputStatus.SUCCEEDED, StepOutputStatus.PAUSED].includes(stepOutput.status)) {
return null
}
if (stepOutput.type === FlowActionType.LOOP_ON_ITEMS) {
const loopOutput = new LoopStepOutput(stepOutput)
const iterations = loopOutput.output?.iterations ?? []
const newIterations: Record<string, StepOutput>[] = []
for (const iteration of iterations) {
const newSteps: Record<string, StepOutput> = {}
for (const [step, output] of Object.entries(iteration)) {
const newOutput = await insertSuccessStepsOrPausedRecursively(output)
if (!isNil(newOutput)) {
newSteps[step] = newOutput
}
}
newIterations.push(newSteps)
}
return loopOutput.setIterations(newIterations)
}
return stepOutput
}

Some files were not shown because too many files have changed in this diff Show More