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:
18
activepieces-fork/packages/cli/.eslintrc.json
Normal file
18
activepieces-fork/packages/cli/.eslintrc.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"extends": ["../../.eslintrc.json"],
|
||||
"ignorePatterns": ["!**/*"],
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
|
||||
"rules": {}
|
||||
},
|
||||
{
|
||||
"files": ["*.ts", "*.tsx"],
|
||||
"rules": {}
|
||||
},
|
||||
{
|
||||
"files": ["*.js", "*.jsx"],
|
||||
"rules": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
7
activepieces-fork/packages/cli/README.md
Normal file
7
activepieces-fork/packages/cli/README.md
Normal 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).
|
||||
2467
activepieces-fork/packages/cli/assets/db.json
Normal file
2467
activepieces-fork/packages/cli/assets/db.json
Normal file
File diff suppressed because it is too large
Load Diff
11
activepieces-fork/packages/cli/jest.config.ts
Normal file
11
activepieces-fork/packages/cli/jest.config.ts
Normal 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',
|
||||
};
|
||||
16
activepieces-fork/packages/cli/package.json
Normal file
16
activepieces-fork/packages/cli/package.json
Normal 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": "*"
|
||||
}
|
||||
}
|
||||
|
||||
29
activepieces-fork/packages/cli/project.json
Normal file
29
activepieces-fork/packages/cli/project.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
44
activepieces-fork/packages/cli/src/index.ts
Normal file
44
activepieces-fork/packages/cli/src/index.ts
Normal 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);
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
247
activepieces-fork/packages/cli/src/lib/commands/create-piece.ts
Normal file
247
activepieces-fork/packages/cli/src/lib/commands/create-piece.ts
Normal 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);
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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`)
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
@@ -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
|
||||
});
|
||||
});
|
||||
@@ -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
|
||||
});
|
||||
});
|
||||
4
activepieces-fork/packages/cli/src/lib/utils/exec.ts
Normal file
4
activepieces-fork/packages/cli/src/lib/utils/exec.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { exec as execCallback } from 'node:child_process';
|
||||
import { promisify } from 'node:util';
|
||||
|
||||
export const exec = promisify(execCallback);
|
||||
97
activepieces-fork/packages/cli/src/lib/utils/files.ts
Normal file
97
activepieces-fork/packages/cli/src/lib/utils/files.ts
Normal 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 });
|
||||
};
|
||||
176
activepieces-fork/packages/cli/src/lib/utils/piece-utils.ts
Normal file
176
activepieces-fork/packages/cli/src/lib/utils/piece-utils.ts
Normal 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;
|
||||
}
|
||||
16
activepieces-fork/packages/cli/tsconfig.json
Normal file
16
activepieces-fork/packages/cli/tsconfig.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
},
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.spec.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
11
activepieces-fork/packages/cli/tsconfig.lib.json
Normal file
11
activepieces-fork/packages/cli/tsconfig.lib.json
Normal 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"]
|
||||
}
|
||||
14
activepieces-fork/packages/cli/tsconfig.spec.json
Normal file
14
activepieces-fork/packages/cli/tsconfig.spec.json
Normal 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"
|
||||
]
|
||||
}
|
||||
36
activepieces-fork/packages/ee/LICENSE
Normal file
36
activepieces-fork/packages/ee/LICENSE
Normal 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.
|
||||
18
activepieces-fork/packages/ee/shared/.eslintrc.json
Normal file
18
activepieces-fork/packages/ee/shared/.eslintrc.json
Normal 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": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
7
activepieces-fork/packages/ee/shared/README.md
Normal file
7
activepieces-fork/packages/ee/shared/README.md
Normal 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.
|
||||
5
activepieces-fork/packages/ee/shared/package.json
Normal file
5
activepieces-fork/packages/ee/shared/package.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "@activepieces/ee-shared",
|
||||
"version": "0.0.11",
|
||||
"type": "commonjs"
|
||||
}
|
||||
23
activepieces-fork/packages/ee/shared/project.json
Normal file
23
activepieces-fork/packages/ee/shared/project.json
Normal 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": []
|
||||
}
|
||||
18
activepieces-fork/packages/ee/shared/src/index.ts
Normal file
18
activepieces-fork/packages/ee/shared/src/index.ts
Normal 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'
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './alerts-dto'
|
||||
export * from './alerts-requests'
|
||||
@@ -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>
|
||||
@@ -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
|
||||
}".`
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
],
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './requests'
|
||||
@@ -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>
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './enterprise-local-authn'
|
||||
export * from './access-control-list'
|
||||
135
activepieces-fork/packages/ee/shared/src/lib/billing/index.ts
Normal file
135
activepieces-fork/packages/ee/shared/src/lib/billing/index.ts
Normal 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
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -0,0 +1 @@
|
||||
export * from './managed-authn-requests'
|
||||
@@ -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>
|
||||
@@ -0,0 +1 @@
|
||||
export * from './oauth-app'
|
||||
@@ -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>
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './otp-model'
|
||||
export * from './otp-requests'
|
||||
export * from './otp-type'
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export enum OtpType {
|
||||
EMAIL_VERIFICATION = 'EMAIL_VERIFICATION',
|
||||
PASSWORD_RESET = 'PASSWORD_RESET',
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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',
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './app-credentials'
|
||||
export * from './app-credentials-requests'
|
||||
@@ -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',
|
||||
}
|
||||
@@ -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>
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './connection-key'
|
||||
export * from './connection-requests'
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './signing-key-model'
|
||||
export * from './signing-key-response'
|
||||
export * from './signing-key.request'
|
||||
@@ -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>
|
||||
@@ -0,0 +1,5 @@
|
||||
import { SigningKey } from './signing-key-model'
|
||||
|
||||
export type AddSigningKeyResponse = SigningKey & {
|
||||
privateKey: string
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { Static, Type } from '@sinclair/typebox'
|
||||
|
||||
export const AddSigningKeyRequestBody = Type.Object({
|
||||
displayName: Type.String(),
|
||||
})
|
||||
|
||||
export type AddSigningKeyRequestBody = Static<typeof AddSigningKeyRequestBody>
|
||||
@@ -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>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from './flow-template.request'
|
||||
@@ -0,0 +1 @@
|
||||
export * from './flow-template'
|
||||
19
activepieces-fork/packages/ee/shared/tsconfig.json
Normal file
19
activepieces-fork/packages/ee/shared/tsconfig.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
10
activepieces-fork/packages/ee/shared/tsconfig.lib.json
Normal file
10
activepieces-fork/packages/ee/shared/tsconfig.lib.json
Normal 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"]
|
||||
}
|
||||
17
activepieces-fork/packages/ee/ui/.eslintrc.json
Normal file
17
activepieces-fork/packages/ee/ui/.eslintrc.json
Normal 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"]
|
||||
}
|
||||
]
|
||||
}
|
||||
18
activepieces-fork/packages/ee/ui/embed-sdk/.eslintrc.json
Normal file
18
activepieces-fork/packages/ee/ui/embed-sdk/.eslintrc.json
Normal 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": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
9
activepieces-fork/packages/ee/ui/embed-sdk/README.md
Normal file
9
activepieces-fork/packages/ee/ui/embed-sdk/README.md
Normal 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.
|
||||
7
activepieces-fork/packages/ee/ui/embed-sdk/package.json
Normal file
7
activepieces-fork/packages/ee/ui/embed-sdk/package.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "ee-embed-sdk",
|
||||
"version": "0.8.1",
|
||||
"type": "commonjs",
|
||||
"main": "./src/index.js",
|
||||
"typings": "./src/index.d.ts"
|
||||
}
|
||||
46
activepieces-fork/packages/ee/ui/embed-sdk/project.json
Normal file
46
activepieces-fork/packages/ee/ui/embed-sdk/project.json
Normal 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": []
|
||||
}
|
||||
618
activepieces-fork/packages/ee/ui/embed-sdk/src/index.ts
Normal file
618
activepieces-fork/packages/ee/ui/embed-sdk/src/index.ts
Normal 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;
|
||||
19
activepieces-fork/packages/ee/ui/embed-sdk/tsconfig.json
Normal file
19
activepieces-fork/packages/ee/ui/embed-sdk/tsconfig.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
10
activepieces-fork/packages/ee/ui/embed-sdk/tsconfig.lib.json
Normal file
10
activepieces-fork/packages/ee/ui/embed-sdk/tsconfig.lib.json
Normal 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"]
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
const { composePlugins, withNx } = require('@nx/webpack');
|
||||
|
||||
module.exports = composePlugins(withNx(), (config) => {
|
||||
return config;
|
||||
});
|
||||
21
activepieces-fork/packages/engine/.eslintrc.json
Normal file
21
activepieces-fork/packages/engine/.eslintrc.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"extends": [
|
||||
"../server/api/.eslintrc.json"
|
||||
],
|
||||
"overrides": [
|
||||
{
|
||||
"files": [
|
||||
"*.ts",
|
||||
"*.js"
|
||||
],
|
||||
"parserOptions": {
|
||||
"project": [
|
||||
"packages/engine/tsconfig.*?.json"
|
||||
]
|
||||
},
|
||||
"rules": {
|
||||
"no-console": "off"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
11
activepieces-fork/packages/engine/README.md
Normal file
11
activepieces-fork/packages/engine/README.md
Normal 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).
|
||||
23
activepieces-fork/packages/engine/jest.config.ts
Normal file
23
activepieces-fork/packages/engine/jest.config.ts
Normal 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',
|
||||
};
|
||||
6
activepieces-fork/packages/engine/package.json
Normal file
6
activepieces-fork/packages/engine/package.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "@activepieces/engine",
|
||||
"version": "0.7.0",
|
||||
"type": "commonjs"
|
||||
}
|
||||
|
||||
85
activepieces-fork/packages/engine/project.json
Normal file
85
activepieces-fork/packages/engine/project.json
Normal 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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
},
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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>
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 + '/'
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
}
|
||||
@@ -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)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
},
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
284
activepieces-fork/packages/engine/src/lib/handler/router-executor.ts
Executable file
284
activepieces-fork/packages/engine/src/lib/handler/router-executor.ts
Executable 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
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
253
activepieces-fork/packages/engine/src/lib/helper/piece-helper.ts
Normal file
253
activepieces-fork/packages/engine/src/lib/helper/piece-helper.ts
Normal 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
|
||||
}
|
||||
208
activepieces-fork/packages/engine/src/lib/helper/piece-loader.ts
Normal file
208
activepieces-fork/packages/engine/src/lib/helper/piece-loader.ts
Normal 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
|
||||
}
|
||||
|
||||
@@ -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[]
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user