- 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>
254 lines
9.3 KiB
TypeScript
254 lines
9.3 KiB
TypeScript
import { ContextVersion } from '@activepieces/pieces-framework'
|
|
import { applyFunctionToValues, isNil, isString } from '@activepieces/shared'
|
|
import replaceAsync from 'string-replace-async'
|
|
import { initCodeSandbox } from '../core/code/code-sandbox'
|
|
import { FlowExecutorContext } from '../handler/context/flow-execution-context'
|
|
import { createConnectionService } from '../services/connections.service'
|
|
import { utils } from '../utils'
|
|
|
|
const VARIABLE_PATTERN = /\{\{(.*?)\}\}/g
|
|
const CONNECTIONS = 'connections'
|
|
const FLATTEN_NESTED_KEYS_PATTERN = /\{\{\s*flattenNestedKeys(.*?)\}\}/g
|
|
|
|
|
|
export const createPropsResolver = ({ engineToken, projectId, apiUrl, contextVersion }: PropsResolverParams) => {
|
|
return {
|
|
resolve: async <T = unknown>(params: ResolveInputParams): Promise<ResolveResult<T>> => {
|
|
const { unresolvedInput, executionState } = params
|
|
if (isNil(unresolvedInput)) {
|
|
return {
|
|
resolvedInput: unresolvedInput as T,
|
|
censoredInput: unresolvedInput,
|
|
}
|
|
}
|
|
const currentState = executionState.currentState()
|
|
const resolveOptions = {
|
|
engineToken,
|
|
projectId,
|
|
apiUrl,
|
|
currentState,
|
|
}
|
|
const resolvedInput = await applyFunctionToValues<T>(
|
|
unresolvedInput,
|
|
(token) => resolveInputAsync({
|
|
...resolveOptions,
|
|
input: token,
|
|
censoredInput: false,
|
|
contextVersion,
|
|
}))
|
|
const censoredInput = await applyFunctionToValues<T>(
|
|
unresolvedInput,
|
|
(token) => resolveInputAsync({
|
|
...resolveOptions,
|
|
input: token,
|
|
censoredInput: true,
|
|
contextVersion,
|
|
}))
|
|
return {
|
|
resolvedInput,
|
|
censoredInput,
|
|
}
|
|
},
|
|
}
|
|
}
|
|
|
|
const mergeFlattenedKeysArraysIntoOneArray = async (token: string, partsThatNeedResolving: string[],
|
|
resolveOptions: Pick<ResolveInputInternalParams, 'engineToken' | 'projectId' | 'apiUrl' | 'currentState' | 'censoredInput'>,
|
|
contextVersion: ContextVersion | undefined,
|
|
) => {
|
|
const resolvedValues: Record<string, unknown> = {}
|
|
let longestResultLength = 0
|
|
for (const tokenPart of partsThatNeedResolving) {
|
|
const variableName = tokenPart.substring(2, tokenPart.length - 2)
|
|
resolvedValues[tokenPart] = await resolveSingleToken({
|
|
...resolveOptions,
|
|
variableName,
|
|
contextVersion,
|
|
})
|
|
if (Array.isArray(resolvedValues[tokenPart])) {
|
|
longestResultLength = Math.max(longestResultLength, resolvedValues[tokenPart].length)
|
|
}
|
|
}
|
|
const result = new Array(longestResultLength).fill(null).map((_, index) => {
|
|
return Object.entries(resolvedValues).reduce((acc, [tokenPart, value]) => {
|
|
const valueToUse = (Array.isArray(value) ? value[index] : value) ?? ''
|
|
acc = acc.replace(tokenPart, isString(valueToUse) ? valueToUse : JSON.stringify(valueToUse))
|
|
return acc
|
|
}, token)
|
|
})
|
|
return result
|
|
}
|
|
|
|
export type PropsResolver = ReturnType<typeof createPropsResolver>
|
|
/**
|
|
* input: `Hello {{firstName}} {{lastName}}`
|
|
* tokenThatNeedResolving: [`{{firstName}}`, `{{lastName}}`]
|
|
*/
|
|
async function resolveInputAsync(params: ResolveInputInternalParams): Promise<unknown> {
|
|
const { input, currentState, engineToken, projectId, apiUrl, censoredInput } = params
|
|
const tokensThatNeedResolving = input.match(VARIABLE_PATTERN)
|
|
const inputContainsOnlyOneTokenToResolve = tokensThatNeedResolving !== null && tokensThatNeedResolving.length === 1 && tokensThatNeedResolving[0] === input
|
|
const resolveOptions = {
|
|
engineToken,
|
|
projectId,
|
|
apiUrl,
|
|
currentState,
|
|
censoredInput,
|
|
}
|
|
|
|
if (inputContainsOnlyOneTokenToResolve) {
|
|
const trimmedInput = input.trim()
|
|
const variableName = trimmedInput.substring(2, trimmedInput.length - 2)
|
|
return resolveSingleToken({
|
|
...resolveOptions,
|
|
variableName,
|
|
contextVersion: params.contextVersion,
|
|
})
|
|
}
|
|
const inputIncludesFlattenNestedKeysTokens = input.match(FLATTEN_NESTED_KEYS_PATTERN)
|
|
if (!isNil(inputIncludesFlattenNestedKeysTokens) && !isNil(tokensThatNeedResolving)) {
|
|
return mergeFlattenedKeysArraysIntoOneArray(input, tokensThatNeedResolving, resolveOptions, params.contextVersion)
|
|
}
|
|
|
|
return replaceAsync(input, VARIABLE_PATTERN, async (_fullMatch, variableName) => {
|
|
const result = await resolveSingleToken({
|
|
...resolveOptions,
|
|
variableName,
|
|
contextVersion: params.contextVersion,
|
|
})
|
|
return isString(result) ? result : JSON.stringify(result)
|
|
})
|
|
}
|
|
|
|
async function resolveSingleToken(params: ResolveSingleTokenParams): Promise<unknown> {
|
|
const { variableName, currentState } = params
|
|
const isConnection = variableName.startsWith(CONNECTIONS)
|
|
if (isConnection) {
|
|
return handleConnection(params)
|
|
}
|
|
return evalInScope(variableName, { ...currentState }, { flattenNestedKeys })
|
|
}
|
|
|
|
async function handleConnection(params: ResolveSingleTokenParams): Promise<unknown> {
|
|
const { variableName, engineToken, projectId, apiUrl, censoredInput } = params
|
|
const connectionName = parseConnectionNameOnly(variableName)
|
|
if (isNil(connectionName)) {
|
|
return ''
|
|
}
|
|
if (censoredInput) {
|
|
return '**REDACTED**'
|
|
}
|
|
const connection = await createConnectionService({ engineToken, projectId, apiUrl, contextVersion: params.contextVersion }).obtain(connectionName)
|
|
const pathAfterConnectionName = parsePathAfterConnectionName(variableName, connectionName)
|
|
if (isNil(pathAfterConnectionName) || pathAfterConnectionName.length === 0) {
|
|
return connection
|
|
}
|
|
return evalInScope(pathAfterConnectionName, { connection }, { flattenNestedKeys })
|
|
}
|
|
|
|
function parsePathAfterConnectionName(variableName: string, connectionName: string): string | null {
|
|
if (variableName.includes('[')) {
|
|
return variableName.substring(`connections.['${connectionName}']`.length)
|
|
}
|
|
const cp = variableName.substring(`connections.${connectionName}`.length)
|
|
if (cp.length === 0) {
|
|
return cp
|
|
}
|
|
return `connection${cp}`
|
|
}
|
|
|
|
function parseConnectionNameOnly(variableName: string): string | null {
|
|
const connectionWithNewFormatSquareBrackets = variableName.includes('[')
|
|
if (connectionWithNewFormatSquareBrackets) {
|
|
return parseSquareBracketConnectionPath(variableName)
|
|
}
|
|
// {{connections.connectionName.path}}
|
|
// This does not work If connectionName contains .
|
|
return variableName.split('.')?.[1]
|
|
}
|
|
|
|
function parseSquareBracketConnectionPath(variableName: string): string | null {
|
|
// Find the connection name inside {{connections['connectionName'].path}}
|
|
const matches = variableName.match(/\['([^']+)'\]/g)
|
|
if (matches && matches.length >= 1) {
|
|
// Remove the square brackets and quotes from the connection name
|
|
|
|
const secondPath = matches[0].replace(/\['|'\]/g, '')
|
|
return secondPath
|
|
}
|
|
return null
|
|
}
|
|
|
|
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
async function evalInScope(js: string, contextAsScope: Record<string, unknown>, functions: Record<string, Function>): Promise<unknown> {
|
|
const { data: result, error: resultError } = await utils.tryCatchAndThrowOnEngineError((async () => {
|
|
const codeSandbox = await initCodeSandbox()
|
|
|
|
const result = await codeSandbox.runScript({
|
|
script: js,
|
|
scriptContext: contextAsScope,
|
|
functions,
|
|
})
|
|
return result ?? ''
|
|
}))
|
|
|
|
if (resultError) {
|
|
console.warn('[evalInScope] Error evaluating variable', resultError)
|
|
return ''
|
|
}
|
|
return result ?? ''
|
|
}
|
|
|
|
function flattenNestedKeys(data: unknown, pathToMatch: string[]): unknown[] {
|
|
if (typeof data === 'object' && data !== null && !Array.isArray(data)) {
|
|
for (const [key, value] of Object.entries(data as Record<string, unknown>)) {
|
|
if (key === pathToMatch[0]) {
|
|
return flattenNestedKeys(value, pathToMatch.slice(1))
|
|
}
|
|
}
|
|
}
|
|
else if (Array.isArray(data)) {
|
|
return data.flatMap((d) => flattenNestedKeys(d, pathToMatch))
|
|
}
|
|
else if (pathToMatch.length === 0) {
|
|
return [data]
|
|
}
|
|
return []
|
|
}
|
|
|
|
type ResolveSingleTokenParams = {
|
|
variableName: string
|
|
currentState: Record<string, unknown>
|
|
engineToken: string
|
|
projectId: string
|
|
apiUrl: string
|
|
censoredInput: boolean
|
|
contextVersion: ContextVersion | undefined
|
|
}
|
|
|
|
type ResolveInputInternalParams = {
|
|
input: string
|
|
engineToken: string
|
|
projectId: string
|
|
apiUrl: string
|
|
censoredInput: boolean
|
|
currentState: Record<string, unknown>
|
|
contextVersion: ContextVersion | undefined
|
|
}
|
|
|
|
type ResolveInputParams = {
|
|
unresolvedInput: unknown
|
|
executionState: FlowExecutorContext
|
|
}
|
|
|
|
type ResolveResult<T = unknown> = {
|
|
resolvedInput: T
|
|
censoredInput: unknown
|
|
}
|
|
|
|
type PropsResolverParams = {
|
|
engineToken: string
|
|
projectId: string
|
|
apiUrl: string
|
|
contextVersion: ContextVersion | undefined
|
|
} |