From 633f47ff9a06e78862326fdd893055db1df896b3 Mon Sep 17 00:00:00 2001 From: Apoorv Taneja Date: Tue, 10 Dec 2024 00:04:59 +0530 Subject: [PATCH] run prettier on changed file --- js/src/cli/triggers.ts | 2 +- js/src/constants.js | 10 +- js/src/frameworks/cloudflare.ts | 7 +- js/src/frameworks/langchain.spec.ts | 2 +- js/src/frameworks/langchain.ts | 16 +- js/src/frameworks/openai.ts | 392 +++++----- js/src/frameworks/vercel.ts | 37 +- js/src/index.ts | 16 +- js/src/sdk/actionRegistry.ts | 282 ++++---- js/src/sdk/base.toolset.spec.ts | 292 ++++---- js/src/sdk/base.toolset.ts | 724 +++++++++++-------- js/src/sdk/index.spec.ts | 254 +++---- js/src/sdk/index.ts | 467 +++++++----- js/src/sdk/models/Entity.spec.ts | 18 +- js/src/sdk/models/Entity.ts | 79 +- js/src/sdk/models/actions.spec.ts | 119 +-- js/src/sdk/models/actions.ts | 445 ++++++------ js/src/sdk/models/activeTriggers.spec.ts | 47 +- js/src/sdk/models/activeTriggers.ts | 176 ++--- js/src/sdk/models/apps.spec.ts | 117 +-- js/src/sdk/models/apps.ts | 210 +++--- js/src/sdk/models/backendClient.spec.ts | 38 +- js/src/sdk/models/backendClient.ts | 132 ++-- js/src/sdk/models/connectedAccounts.spec.ts | 101 +-- js/src/sdk/models/connectedAccounts.ts | 372 ++++++---- js/src/sdk/models/integrations.spec.ts | 97 +-- js/src/sdk/models/integrations.ts | 320 ++++---- js/src/sdk/models/triggers.spec.ts | 235 +++--- js/src/sdk/models/triggers.ts | 281 +++---- js/src/sdk/utils/base/batchProcessor.ts | 61 +- js/src/sdk/utils/common.ts | 8 +- js/src/sdk/utils/composioContext.ts | 12 +- js/src/sdk/utils/config.ts | 186 ++--- js/src/sdk/utils/constants.ts | 5 +- js/src/sdk/utils/error.ts | 319 ++++---- js/src/sdk/utils/errors/index.ts | 123 ++-- js/src/sdk/utils/errors/src/composioError.ts | 218 +++--- js/src/sdk/utils/errors/src/constants.ts | 194 ++--- js/src/sdk/utils/errors/src/formatter.ts | 158 ++-- js/src/sdk/utils/telemetry/events.ts | 8 +- js/src/sdk/utils/telemetry/index.ts | 104 +-- js/src/utils/external.ts | 110 +-- js/src/utils/logger.ts | 4 +- 43 files changed, 3653 insertions(+), 3145 deletions(-) diff --git a/js/src/cli/triggers.ts b/js/src/cli/triggers.ts index 9c58f1a802f..162383a280d 100644 --- a/js/src/cli/triggers.ts +++ b/js/src/cli/triggers.ts @@ -139,7 +139,7 @@ export class TriggerAdd { } } - const triggerSetupData = await composioClient.triggers.setup({ + const triggerSetupData = await composioClient.triggers.setup({ connectedAccountId: connection.id, triggerName, config: configValue, diff --git a/js/src/constants.js b/js/src/constants.js index f81ed51a737..157d1e86ad7 100644 --- a/js/src/constants.js +++ b/js/src/constants.js @@ -8,10 +8,10 @@ const ACTIONS = { // actions list end here }; -const COMPOSIO_VERSION = `0.3.0` +const COMPOSIO_VERSION = `0.3.0`; module.exports = { - APPS, - ACTIONS, - COMPOSIO_VERSION -} + APPS, + ACTIONS, + COMPOSIO_VERSION, +}; diff --git a/js/src/frameworks/cloudflare.ts b/js/src/frameworks/cloudflare.ts index 5f4bbb0d270..e943be50225 100644 --- a/js/src/frameworks/cloudflare.ts +++ b/js/src/frameworks/cloudflare.ts @@ -102,8 +102,11 @@ export class CloudflareToolSet extends BaseComposioToolSet { return JSON.stringify( await this.executeAction({ action: tool.name, - params: typeof tool.arguments === "string" ? JSON.parse(tool.arguments) : tool.arguments, - entityId: entityId || this.entityId + params: + typeof tool.arguments === "string" + ? JSON.parse(tool.arguments) + : tool.arguments, + entityId: entityId || this.entityId, }) ); } diff --git a/js/src/frameworks/langchain.spec.ts b/js/src/frameworks/langchain.spec.ts index 7e3be140fe6..d7da4a25aeb 100644 --- a/js/src/frameworks/langchain.spec.ts +++ b/js/src/frameworks/langchain.spec.ts @@ -66,7 +66,7 @@ describe("Apps class tests", () => { repo: "achievementsof.life", }, entityId: "default", - connectedAccountId: "db3c8d95-73e9-474e-8ae8-edfbdaab98b1" + connectedAccountId: "db3c8d95-73e9-474e-8ae8-edfbdaab98b1", }); expect(actionOuput).toHaveProperty("successfull", true); diff --git a/js/src/frameworks/langchain.ts b/js/src/frameworks/langchain.ts index 6160db14870..ab979249d5c 100644 --- a/js/src/frameworks/langchain.ts +++ b/js/src/frameworks/langchain.ts @@ -39,13 +39,15 @@ export class LangchainToolSet extends BaseComposioToolSet { const action = schema["name"]; const description = schema["description"]; - const func = async (...kwargs: any[]): Promise => { - return JSON.stringify(await this.executeAction({ - action, - params: kwargs[0], - entityId: entityId || this.entityId - })); - }; + const func = async (...kwargs: any[]): Promise => { + return JSON.stringify( + await this.executeAction({ + action, + params: kwargs[0], + entityId: entityId || this.entityId, + }) + ); + }; const parameters = jsonSchemaToModel(schema["parameters"]); diff --git a/js/src/frameworks/openai.ts b/js/src/frameworks/openai.ts index eea47c6c4e1..535a52e0e81 100644 --- a/js/src/frameworks/openai.ts +++ b/js/src/frameworks/openai.ts @@ -12,193 +12,229 @@ type Optional = T | null; type Sequence = Array; export class OpenAIToolSet extends BaseComposioToolSet { - static FRAMEWORK_NAME = "openai"; - static DEFAULT_ENTITY_ID = "default"; - - /** - * Composio toolset for OpenAI framework. - * - * Example: - * ```typescript - * - * ``` - */ - constructor( - config: { - apiKey?: Optional, - baseUrl?: Optional, - entityId?: string, - workspaceConfig?: WorkspaceConfig - }={} - ) { - super( - config.apiKey || null, - config.baseUrl || COMPOSIO_BASE_URL, - OpenAIToolSet.FRAMEWORK_NAME, - config.entityId || OpenAIToolSet.DEFAULT_ENTITY_ID, - config.workspaceConfig || Workspace.Host() + static FRAMEWORK_NAME = "openai"; + static DEFAULT_ENTITY_ID = "default"; + + /** + * Composio toolset for OpenAI framework. + * + * Example: + * ```typescript + * + * ``` + */ + constructor( + config: { + apiKey?: Optional; + baseUrl?: Optional; + entityId?: string; + workspaceConfig?: WorkspaceConfig; + } = {} + ) { + super( + config.apiKey || null, + config.baseUrl || COMPOSIO_BASE_URL, + OpenAIToolSet.FRAMEWORK_NAME, + config.entityId || OpenAIToolSet.DEFAULT_ENTITY_ID, + config.workspaceConfig || Workspace.Host() + ); + } + + async getTools( + filters: { + actions?: Sequence; + apps?: Sequence; + tags?: Optional>; + useCase?: Optional; + useCaseLimit?: Optional; + filterByAvailableApps?: Optional; + }, + entityId?: Optional + ): Promise> { + const mainActions = await this.getToolsSchema(filters, entityId); + return ( + mainActions.map( + (action: NonNullable[0]) => { + const formattedSchema: OpenAI.FunctionDefinition = { + name: action.name!, + description: action.description!, + parameters: action.parameters!, + }; + const tool: OpenAI.ChatCompletionTool = { + type: "function", + function: formattedSchema, + }; + return tool; + } + ) || [] + ); + } + + async executeToolCall( + tool: OpenAI.ChatCompletionMessageToolCall, + entityId: Optional = null + ): Promise { + return JSON.stringify( + await this.executeAction({ + action: tool.function.name, + params: JSON.parse(tool.function.arguments), + entityId: entityId || this.entityId, + }) + ); + } + + async handleToolCall( + chatCompletion: OpenAI.ChatCompletion, + entityId: Optional = null + ): Promise> { + const outputs = []; + for (const message of chatCompletion.choices) { + if (message.message.tool_calls) { + outputs.push( + await this.executeToolCall(message.message.tool_calls[0], entityId) ); + } } + return outputs; + } + + async handleAssistantMessage( + run: OpenAI.Beta.Threads.Run, + entityId: Optional = null + ): Promise< + Array + > { + const tool_calls = + run.required_action?.submit_tool_outputs?.tool_calls || []; + const tool_outputs: Array = + await Promise.all( + tool_calls.map(async (tool_call) => { + logger.debug( + `Executing tool call with ID: ${tool_call.function.name} and parameters: ${JSON.stringify(tool_call.function.arguments)}` + ); + const tool_response = await this.executeToolCall( + tool_call as OpenAI.ChatCompletionMessageToolCall, + entityId || this.entityId + ); + logger.debug( + `Received tool response: ${JSON.stringify(tool_response)}` + ); + return { + tool_call_id: tool_call.id, + output: JSON.stringify(tool_response), + }; + }) + ); + return tool_outputs; + } + + async *waitAndHandleAssistantStreamToolCalls( + client: OpenAI, + runStream: Stream, + thread: OpenAI.Beta.Threads.Thread, + entityId: string | null = null + ): AsyncGenerator { + let runId = null; + + // Start processing the runStream events + for await (const event of runStream) { + yield event; // Yield each event from the stream as it arrives + + if (event.event === "thread.run.created") { + const { id } = event.data; + runId = id; + } + + if (!runId) { + continue; + } + + // Handle the 'requires_action' event + if (event.event === "thread.run.requires_action") { + const toolOutputs = await this.handleAssistantMessage( + event.data, + entityId + ); - async getTools( - filters: { - actions?: Sequence; - apps?: Sequence; - tags?: Optional>; - useCase?: Optional; - useCaseLimit?: Optional; - filterByAvailableApps?: Optional; - }, - entityId?: Optional - ): Promise> { - const mainActions = await this.getToolsSchema(filters, entityId); - return mainActions.map((action: NonNullable[0]) => { - const formattedSchema: OpenAI.FunctionDefinition = { - name: action.name!, - description: action.description!, - parameters: action.parameters!, - }; - const tool: OpenAI.ChatCompletionTool = { - type: "function", - function: formattedSchema - } - return tool; - }) || []; - } - - - async executeToolCall( - tool: OpenAI.ChatCompletionMessageToolCall, - entityId: Optional = null - ): Promise { - return JSON.stringify(await this.executeAction({ - action: tool.function.name, - params: JSON.parse(tool.function.arguments), - entityId: entityId || this.entityId - })); + // Submit the tool outputs + await client.beta.threads.runs.submitToolOutputs(thread.id, runId, { + tool_outputs: toolOutputs, + }); + } + + // Break if the run status becomes inactive + if ( + [ + "thread.run.completed", + "thread.run.failed", + "thread.run.cancelled", + "thread.run.expired", + ].includes(event.event) + ) { + break; + } } - - async handleToolCall( - chatCompletion: OpenAI.ChatCompletion, - entityId: Optional = null - ): Promise> { - const outputs = []; - for (const message of chatCompletion.choices) { - if (message.message.tool_calls) { - outputs.push(await this.executeToolCall(message.message.tool_calls[0], entityId)); - } - } - return outputs; + if (!runId) { + throw new Error("No run ID found"); } + // Handle any final actions after the stream ends + let finalRun = await client.beta.threads.runs.retrieve(thread.id, runId); - async handleAssistantMessage( - run: OpenAI.Beta.Threads.Run, - entityId: Optional = null - ): Promise> { - const tool_calls = run.required_action?.submit_tool_outputs?.tool_calls || []; - const tool_outputs: Array = await Promise.all( - tool_calls.map(async (tool_call) => { - logger.debug(`Executing tool call with ID: ${tool_call.function.name} and parameters: ${JSON.stringify(tool_call.function.arguments)}`); - const tool_response = await this.executeToolCall( - tool_call as OpenAI.ChatCompletionMessageToolCall, - entityId || this.entityId - ); - logger.debug(`Received tool response: ${JSON.stringify(tool_response)}`); - return { - tool_call_id: tool_call.id, - output: JSON.stringify(tool_response), - }; - }) + while ( + ["queued", "in_progress", "requires_action"].includes(finalRun.status) + ) { + if (finalRun.status === "requires_action") { + const toolOutputs = await this.handleAssistantMessage( + finalRun, + entityId ); - return tool_outputs; - } - async *waitAndHandleAssistantStreamToolCalls( - client: OpenAI, - runStream: Stream, - thread: OpenAI.Beta.Threads.Thread, - entityId: string | null = null - ): AsyncGenerator { - let runId = null; - - // Start processing the runStream events - for await (const event of runStream) { - yield event; // Yield each event from the stream as it arrives - - if (event.event === 'thread.run.created') { - const { id } = event.data; - runId = id; - } - - if(!runId) { - continue; - } - - // Handle the 'requires_action' event - if (event.event === 'thread.run.requires_action') { - const toolOutputs = await this.handleAssistantMessage(event.data, entityId); - - // Submit the tool outputs - await client.beta.threads.runs.submitToolOutputs(thread.id, runId, { - tool_outputs: toolOutputs - }); - } - - // Break if the run status becomes inactive - if (['thread.run.completed', 'thread.run.failed', 'thread.run.cancelled', 'thread.run.expired'].includes(event.event)) { - break; - } - } - - if(!runId) { - throw new Error("No run ID found"); - } - - // Handle any final actions after the stream ends - let finalRun = await client.beta.threads.runs.retrieve(thread.id, runId); - - while (["queued", "in_progress", "requires_action"].includes(finalRun.status)) { - if (finalRun.status === "requires_action") { - const toolOutputs = await this.handleAssistantMessage(finalRun, entityId); - - // Submit tool outputs - finalRun = await client.beta.threads.runs.submitToolOutputs(thread.id, runId, { - tool_outputs: toolOutputs - }); - } else { - // Update the run status - finalRun = await client.beta.threads.runs.retrieve(thread.id, runId); - await new Promise(resolve => setTimeout(resolve, 500)); // Wait before rechecking - } - } + // Submit tool outputs + finalRun = await client.beta.threads.runs.submitToolOutputs( + thread.id, + runId, + { + tool_outputs: toolOutputs, + } + ); + } else { + // Update the run status + finalRun = await client.beta.threads.runs.retrieve(thread.id, runId); + await new Promise((resolve) => setTimeout(resolve, 500)); // Wait before rechecking + } } - - - async waitAndHandleAssistantToolCalls( - client: OpenAI, - run: OpenAI.Beta.Threads.Run, - thread: OpenAI.Beta.Threads.Thread, - entityId: Optional = null - ): Promise { - while (["queued", "in_progress", "requires_action"].includes(run.status)) { - logger.debug(`Current run status: ${run.status}`); - const tool_outputs = await this.handleAssistantMessage(run, entityId || this.entityId); - if (run.status === "requires_action") { - logger.debug(`Submitting tool outputs for run ID: ${run.id} in thread ID: ${thread.id}`); - run = await client.beta.threads.runs.submitToolOutputs( - thread.id, - run.id, - { - tool_outputs: tool_outputs - } - ); - } else { - run = await client.beta.threads.runs.retrieve(thread.id, run.id); - await new Promise(resolve => setTimeout(resolve, 500)); - } - } - return run; + } + + async waitAndHandleAssistantToolCalls( + client: OpenAI, + run: OpenAI.Beta.Threads.Run, + thread: OpenAI.Beta.Threads.Thread, + entityId: Optional = null + ): Promise { + while (["queued", "in_progress", "requires_action"].includes(run.status)) { + logger.debug(`Current run status: ${run.status}`); + const tool_outputs = await this.handleAssistantMessage( + run, + entityId || this.entityId + ); + if (run.status === "requires_action") { + logger.debug( + `Submitting tool outputs for run ID: ${run.id} in thread ID: ${thread.id}` + ); + run = await client.beta.threads.runs.submitToolOutputs( + thread.id, + run.id, + { + tool_outputs: tool_outputs, + } + ); + } else { + run = await client.beta.threads.runs.retrieve(thread.id, run.id); + await new Promise((resolve) => setTimeout(resolve, 500)); + } } -} \ No newline at end of file + return run; + } +} diff --git a/js/src/frameworks/vercel.ts b/js/src/frameworks/vercel.ts index 0a513a7f199..1d2c8b3b290 100644 --- a/js/src/frameworks/vercel.ts +++ b/js/src/frameworks/vercel.ts @@ -6,7 +6,6 @@ import { CEG } from "../sdk/utils/error"; import { SDK_ERROR_CODES } from "../sdk/utils/errors/src/constants"; type Optional = T | null; - const zExecuteToolCallParams = z.object({ actions: z.array(z.string()).optional(), apps: z.array(z.string()).optional(), @@ -18,7 +17,7 @@ const zExecuteToolCallParams = z.object({ tags: z.array(z.string()).optional(), filterByAvailableApps: z.boolean().optional().default(false), -}) +}); export class VercelAIToolSet extends BaseComposioToolSet { constructor( @@ -42,15 +41,17 @@ export class VercelAIToolSet extends BaseComposioToolSet { description: schema.description, parameters, execute: async (params: Record) => { - return await this.executeToolCall({ - name: schema.name, - arguments: JSON.stringify(params) - }, this.entityId); + return await this.executeToolCall( + { + name: schema.name, + arguments: JSON.stringify(params), + }, + this.entityId + ); }, }); } - // change this implementation async getTools(filters: { actions?: Array; @@ -60,16 +61,22 @@ export class VercelAIToolSet extends BaseComposioToolSet { usecaseLimit?: Optional; filterByAvailableApps?: Optional; }): Promise<{ [key: string]: any }> { + const { + apps, + tags, + useCase, + usecaseLimit, + filterByAvailableApps, + actions, + } = zExecuteToolCallParams.parse(filters); - const {apps, tags, useCase, usecaseLimit, filterByAvailableApps, actions} = zExecuteToolCallParams.parse(filters); - const actionsList = await this.client.actions.list({ ...(apps && { apps: apps?.join(",") }), ...(tags && { tags: tags?.join(",") }), ...(useCase && { useCase: useCase }), ...(actions && { actions: actions?.join(",") }), ...(usecaseLimit && { usecaseLimit: usecaseLimit }), - filterByAvailableApps: filterByAvailableApps ?? undefined + filterByAvailableApps: filterByAvailableApps ?? undefined, }); const tools = {}; @@ -81,16 +88,18 @@ export class VercelAIToolSet extends BaseComposioToolSet { return tools; } - async executeToolCall( - tool: { name: string; arguments: unknown; }, + tool: { name: string; arguments: unknown }, entityId: Optional = null ): Promise { return JSON.stringify( await this.executeAction({ action: tool.name, - params: typeof tool.arguments === "string" ? JSON.parse(tool.arguments) : tool.arguments, - entityId: entityId || this.entityId + params: + typeof tool.arguments === "string" + ? JSON.parse(tool.arguments) + : tool.arguments, + entityId: entityId || this.entityId, }) ); } diff --git a/js/src/index.ts b/js/src/index.ts index 21f2e2a5178..befee8b2ee4 100644 --- a/js/src/index.ts +++ b/js/src/index.ts @@ -1,11 +1,21 @@ import { Composio } from "./sdk/index"; import { LangchainToolSet } from "./frameworks/langchain"; -import { OpenAIToolSet } from "./frameworks/openai";; +import { OpenAIToolSet } from "./frameworks/openai"; import { CloudflareToolSet } from "./frameworks/cloudflare"; import { VercelAIToolSet } from "./frameworks/vercel"; import { LangGraphToolSet } from "./frameworks/langgraph"; import { Workspace } from "./env/index"; -const { APPS,ACTIONS } = require("./constants"); +const { APPS, ACTIONS } = require("./constants"); -export { Composio, LangchainToolSet, OpenAIToolSet, CloudflareToolSet, VercelAIToolSet, Workspace ,APPS, ACTIONS, LangGraphToolSet }; +export { + Composio, + LangchainToolSet, + OpenAIToolSet, + CloudflareToolSet, + VercelAIToolSet, + Workspace, + APPS, + ACTIONS, + LangGraphToolSet, +}; diff --git a/js/src/sdk/actionRegistry.ts b/js/src/sdk/actionRegistry.ts index a5b9b3b67f1..596d35f1e34 100644 --- a/js/src/sdk/actionRegistry.ts +++ b/js/src/sdk/actionRegistry.ts @@ -1,10 +1,10 @@ import { - z, - ZodType, - ZodObject, - ZodString, - AnyZodObject, - ZodOptional, + z, + ZodType, + ZodObject, + ZodString, + AnyZodObject, + ZodOptional, } from "zod"; import { zodToJsonSchema, JsonSchema7Type } from "zod-to-json-schema"; import { ActionProxyRequestConfigDTO } from "./client"; @@ -14,160 +14,160 @@ import { CEG } from "./utils/error"; type ExecuteRequest = Omit; export interface CreateActionOptions { - actionName?: string; - toolName?: string; - description?: string; - inputParams: ZodObject<{ [key: string]: ZodString | ZodOptional }>; - callback: ( - inputParams: Record, - authCredentials: Record | undefined, - executeRequest: (data: ExecuteRequest) => Promise - ) => Promise>; + actionName?: string; + toolName?: string; + description?: string; + inputParams: ZodObject<{ [key: string]: ZodString | ZodOptional }>; + callback: ( + inputParams: Record, + authCredentials: Record | undefined, + executeRequest: (data: ExecuteRequest) => Promise + ) => Promise>; } interface ParamsSchema { - definitions: { - input: { - properties: Record; - required?: string[]; - }; + definitions: { + input: { + properties: Record; + required?: string[]; }; + }; } interface ExecuteMetadata { - entityId?: string; - connectionId?: string; + entityId?: string; + connectionId?: string; } export class ActionRegistry { - client: Composio; - customActions: Map; + client: Composio; + customActions: Map; - constructor(client: Composio) { - this.client = client; - this.customActions = new Map(); - } + constructor(client: Composio) { + this.client = client; + this.customActions = new Map(); + } - async createAction( - options: CreateActionOptions - ): Promise> { - const { callback } = options; - if (typeof callback !== "function") { - throw new Error("Callback must be a function"); - } - if (!options.actionName) { - throw new Error("You must provide actionName for this action"); - } - if (!options.inputParams) { - options.inputParams = z.object({}); - } - const params = options.inputParams; - const actionName = options.actionName || callback.name || ""; - const paramsSchema: ParamsSchema = (await zodToJsonSchema(params, { - name: "input", - })) as ParamsSchema; - const _params = paramsSchema.definitions.input.properties; - const composioSchema = { - name: actionName, - description: options.description, - parameters: { - title: actionName, - type: "object", - description: options.description, - required: paramsSchema.definitions.input.required || [], - properties: _params, - }, - response: { - type: "object", - title: "Response for " + actionName, - properties: [], - }, - }; - this.customActions.set(options.actionName?.toLocaleLowerCase() || "", { - metadata: options, - schema: composioSchema, - }); - return composioSchema; + async createAction( + options: CreateActionOptions + ): Promise> { + const { callback } = options; + if (typeof callback !== "function") { + throw new Error("Callback must be a function"); } - - async getActions({ - actions, - }: { - actions: Array; - }): Promise> { - const actionsArr: Array = []; - for (const name of actions) { - const lowerCaseName = name.toLowerCase(); - if (this.customActions.has(lowerCaseName)) { - const action = this.customActions.get(lowerCaseName); - actionsArr.push(action!.schema); - } - } - return actionsArr; + if (!options.actionName) { + throw new Error("You must provide actionName for this action"); } - - async getAllActions(): Promise> { - return Array.from(this.customActions.values()).map((action: any) => action); + if (!options.inputParams) { + options.inputParams = z.object({}); } + const params = options.inputParams; + const actionName = options.actionName || callback.name || ""; + const paramsSchema: ParamsSchema = (await zodToJsonSchema(params, { + name: "input", + })) as ParamsSchema; + const _params = paramsSchema.definitions.input.properties; + const composioSchema = { + name: actionName, + description: options.description, + parameters: { + title: actionName, + type: "object", + description: options.description, + required: paramsSchema.definitions.input.required || [], + properties: _params, + }, + response: { + type: "object", + title: "Response for " + actionName, + properties: [], + }, + }; + this.customActions.set(options.actionName?.toLocaleLowerCase() || "", { + metadata: options, + schema: composioSchema, + }); + return composioSchema; + } - async executeAction( - name: string, - inputParams: Record, - metadata: ExecuteMetadata - ): Promise { - const lowerCaseName = name.toLocaleLowerCase(); - if (!this.customActions.has(lowerCaseName)) { - throw new Error(`Action with name ${name} does not exist`); - } - + async getActions({ + actions, + }: { + actions: Array; + }): Promise> { + const actionsArr: Array = []; + for (const name of actions) { + const lowerCaseName = name.toLowerCase(); + if (this.customActions.has(lowerCaseName)) { const action = this.customActions.get(lowerCaseName); - if (!action) { - throw new Error(`Action with name ${name} could not be retrieved`); - } + actionsArr.push(action!.schema); + } + } + return actionsArr; + } + + async getAllActions(): Promise> { + return Array.from(this.customActions.values()).map((action: any) => action); + } - const { callback, toolName } = action.metadata; - let authCredentials = {}; - if (toolName) { - const entity = await this.client.getEntity(metadata.entityId); - const connection = await entity.getConnection({ - app: toolName, - connectedAccountId: metadata.connectionId - }); - if (!connection) { - throw new Error( - `Connection with app name ${toolName} and entityId ${metadata.entityId} not found` - ); - } - authCredentials = { - headers: connection.connectionParams?.headers, - queryParams: connection.connectionParams?.queryParams, - baseUrl: - connection.connectionParams?.baseUrl || - connection.connectionParams?.base_url, - }; - } - if (typeof callback !== "function") { - throw new Error("Callback must be a function"); - } + async executeAction( + name: string, + inputParams: Record, + metadata: ExecuteMetadata + ): Promise { + const lowerCaseName = name.toLocaleLowerCase(); + if (!this.customActions.has(lowerCaseName)) { + throw new Error(`Action with name ${name} does not exist`); + } - const executeRequest = async (data: ExecuteRequest) => { - try { - const { data: res } = await apiClient.actionsV2.executeActionProxyV2({ - body: { - ...data, - connectedAccountId: metadata?.connectionId - } as ActionProxyRequestConfigDTO, - }); - return res!; - } catch (error) { - throw CEG.handleAllError(error); - } - }; + const action = this.customActions.get(lowerCaseName); + if (!action) { + throw new Error(`Action with name ${name} could not be retrieved`); + } - return await callback( - inputParams, - authCredentials, - (data: ExecuteRequest) => executeRequest(data) + const { callback, toolName } = action.metadata; + let authCredentials = {}; + if (toolName) { + const entity = await this.client.getEntity(metadata.entityId); + const connection = await entity.getConnection({ + app: toolName, + connectedAccountId: metadata.connectionId, + }); + if (!connection) { + throw new Error( + `Connection with app name ${toolName} and entityId ${metadata.entityId} not found` ); + } + authCredentials = { + headers: connection.connectionParams?.headers, + queryParams: connection.connectionParams?.queryParams, + baseUrl: + connection.connectionParams?.baseUrl || + connection.connectionParams?.base_url, + }; } -} \ No newline at end of file + if (typeof callback !== "function") { + throw new Error("Callback must be a function"); + } + + const executeRequest = async (data: ExecuteRequest) => { + try { + const { data: res } = await apiClient.actionsV2.executeActionProxyV2({ + body: { + ...data, + connectedAccountId: metadata?.connectionId, + } as ActionProxyRequestConfigDTO, + }); + return res!; + } catch (error) { + throw CEG.handleAllError(error); + } + }; + + return await callback( + inputParams, + authCredentials, + (data: ExecuteRequest) => executeRequest(data) + ); + } +} diff --git a/js/src/sdk/base.toolset.spec.ts b/js/src/sdk/base.toolset.spec.ts index cc67c381d99..83d60ae25e8 100644 --- a/js/src/sdk/base.toolset.spec.ts +++ b/js/src/sdk/base.toolset.spec.ts @@ -4,156 +4,168 @@ import { getTestConfig } from "../../config/getTestConfig"; import { ActionExecutionResDto, ExecuteActionResDTO } from "./client"; describe("ComposioToolSet class tests", () => { - let toolset: ComposioToolSet; - const testConfig = getTestConfig(); - - beforeAll(() => { - toolset = new ComposioToolSet(testConfig.COMPOSIO_API_KEY, testConfig.BACKEND_HERMES_URL); + let toolset: ComposioToolSet; + const testConfig = getTestConfig(); + + beforeAll(() => { + toolset = new ComposioToolSet( + testConfig.COMPOSIO_API_KEY, + testConfig.BACKEND_HERMES_URL + ); + }); + + it("should create a ComposioToolSet instance", async () => { + const tools = await toolset.getToolsSchema({ apps: ["github"] }); + expect(tools).toBeInstanceOf(Array); + expect(tools).not.toHaveLength(0); + }); + + it("should create a ComposioToolSet instance with apps and tags", async () => { + const tools = await toolset.getToolsSchema({ + apps: ["github"], + tags: ["important"], }); + expect(tools).toBeInstanceOf(Array); + expect(tools).not.toHaveLength(0); + }); - it("should create a ComposioToolSet instance", async() => { - const tools = await toolset.getToolsSchema({ apps: ["github"] }); - expect(tools).toBeInstanceOf(Array); - expect(tools).not.toHaveLength(0); + it("should create a ComposioToolSet instance with actions", async () => { + const tools = await toolset.getActionsSchema({ + actions: ["github_issues_create"], }); - - it("should create a ComposioToolSet instance with apps and tags", async () => { - const tools = await toolset.getToolsSchema({ apps: ["github"], tags: ["important"] }); - expect(tools).toBeInstanceOf(Array); - expect(tools).not.toHaveLength(0); + expect(tools).toBeInstanceOf(Array); + }); + + it("should execute an action", async () => { + const actionName = "github_issues_create"; + const requestBody = { + owner: "utkarsh-dixit", + repo: "speedy", + title: "Test issue", + body: "This is a test issue", + appNames: "github", + }; + + const executionResult = await toolset.executeAction({ + action: actionName, + params: requestBody, + entityId: "default", }); - - it("should create a ComposioToolSet instance with actions", async () => { - const tools = await toolset.getActionsSchema({ actions: ["github_issues_create"] }); - expect(tools).toBeInstanceOf(Array); + expect(executionResult).toBeDefined(); + // @ts-ignore + expect(executionResult).toHaveProperty("successfull", true); + expect(executionResult.data).toBeDefined(); + }); + + it("should execute an action with pre processor", async () => { + const actionName = "github_issues_create"; + const requestBody = { + owner: "utkarsh-dixit", + repo: "speedy", + title: "Test issue", + body: "This is a test issue", + appNames: "github", + }; + + const preProcessor = ({ + action, + toolRequest, + }: { + action: string; + toolRequest: Record; + }) => { + return { + ...toolRequest, + owner: "utkarsh-dixit", + repo: "speedy", + title: "Test issue2", + }; + }; + + const postProcessor = ({ + action, + toolResponse, + }: { + action: string; + toolResponse: ActionExecutionResDto; + }) => { + return { + data: { + ...toolResponse.data, + isPostProcessed: true, + }, + error: toolResponse.error, + successfull: toolResponse.successfull, + }; + }; + + toolset.addPreProcessor(preProcessor); + toolset.addPostProcessor(postProcessor); + + const executionResult = await toolset.executeAction({ + action: actionName, + params: requestBody, + entityId: "default", }); - it("should execute an action", async () => { - - const actionName = "github_issues_create"; - const requestBody = { - owner: "utkarsh-dixit", - repo: "speedy", - title: "Test issue", - body: "This is a test issue", - appNames: "github" - }; - - const executionResult = await toolset.executeAction({ - action: actionName, - params: requestBody, - entityId: "default" - }); - expect(executionResult).toBeDefined(); - // @ts-ignore - expect(executionResult).toHaveProperty('successfull', true); - expect(executionResult.data).toBeDefined(); - - }); + expect(executionResult).toBeDefined(); + // @ts-ignore + expect(executionResult).toHaveProperty("successfull", true); + expect(executionResult.data).toBeDefined(); + expect(executionResult.data.title).toBe("Test issue2"); + expect(executionResult.data.isPostProcessed).toBe(true); + // Remove pre processor and post processor + toolset.removePreProcessor(); - it("should execute an action with pre processor", async () => { - const actionName = "github_issues_create"; - const requestBody = { - owner: "utkarsh-dixit", - repo: "speedy", - title: "Test issue", - body: "This is a test issue", - appNames: "github" - }; - - const preProcessor = ({ action, toolRequest }:{ - action: string, - toolRequest: Record - }) => { - return { - ...toolRequest, - owner: "utkarsh-dixit", - repo: "speedy", - title: "Test issue2", - }; - }; - - const postProcessor = ({ action, toolResponse }:{ - action: string, - toolResponse: ActionExecutionResDto - }) => { - return { - data: { - ...toolResponse.data, - isPostProcessed: true - }, - error: toolResponse.error, - successfull: toolResponse.successfull - }; - }; - - toolset.addPreProcessor(preProcessor); - toolset.addPostProcessor(postProcessor); - - const executionResult = await toolset.executeAction({ - action: actionName, - params: requestBody, - entityId: "default" - }); - - expect(executionResult).toBeDefined(); - // @ts-ignore - expect(executionResult).toHaveProperty('successfull', true); - expect(executionResult.data).toBeDefined(); - expect(executionResult.data.title).toBe("Test issue2"); - expect(executionResult.data.isPostProcessed).toBe(true); - - // Remove pre processor and post processor - toolset.removePreProcessor(); - - const executionResultAfterRemove = await toolset.executeAction({ - action: actionName, - params: requestBody, - entityId: "default" - }); - - expect(executionResultAfterRemove).toBeDefined(); - // @ts-ignore - expect(executionResultAfterRemove).toHaveProperty('successfull', true); - expect(executionResultAfterRemove.data).toBeDefined(); - expect(executionResultAfterRemove.data.title).toBe("Test issue"); + const executionResultAfterRemove = await toolset.executeAction({ + action: actionName, + params: requestBody, + entityId: "default", }); - it("should execute an file upload", async () => { - const ACTION_NAME = "GMAIL_SEND_EMAIL"; - const actions = await toolset.getToolsSchema({ actions: [ACTION_NAME] }); - - // Check if exist - expect(actions[0].parameters.properties["attachment_file_uri_path"]).toBeDefined(); - - const requestBody = { - recipient_email: "himanshu@composio.dev", - subject: "Test email from himanshu", - body: "This is a test email", - attachment_file_uri_path: "https://composio.dev/wp-content/uploads/2024/07/Composio-Logo.webp" - }; - - const executionResult = await toolset.executeAction({ - action: ACTION_NAME, - params: requestBody, - entityId: "default" - }); - expect(executionResult).toBeDefined(); - // @ts-ignore - expect(executionResult).toHaveProperty('successfull', true); - expect(executionResult.data).toBeDefined(); - + expect(executionResultAfterRemove).toBeDefined(); + // @ts-ignore + expect(executionResultAfterRemove).toHaveProperty("successfull", true); + expect(executionResultAfterRemove.data).toBeDefined(); + expect(executionResultAfterRemove.data.title).toBe("Test issue"); + }); + + it("should execute an file upload", async () => { + const ACTION_NAME = "GMAIL_SEND_EMAIL"; + const actions = await toolset.getToolsSchema({ actions: [ACTION_NAME] }); + + // Check if exist + expect( + actions[0].parameters.properties["attachment_file_uri_path"] + ).toBeDefined(); + + const requestBody = { + recipient_email: "himanshu@composio.dev", + subject: "Test email from himanshu", + body: "This is a test email", + attachment_file_uri_path: + "https://composio.dev/wp-content/uploads/2024/07/Composio-Logo.webp", + }; + + const executionResult = await toolset.executeAction({ + action: ACTION_NAME, + params: requestBody, + entityId: "default", }); - - it("should get tools with usecase limit", async () => { - const tools = await toolset.getToolsSchema({ - useCase: "follow user", - apps: ["github"], - useCaseLimit: 1 - }); - - expect(tools.length).toBe(1); + expect(executionResult).toBeDefined(); + // @ts-ignore + expect(executionResult).toHaveProperty("successfull", true); + expect(executionResult.data).toBeDefined(); + }); + + it("should get tools with usecase limit", async () => { + const tools = await toolset.getToolsSchema({ + useCase: "follow user", + apps: ["github"], + useCaseLimit: 1, }); -}); \ No newline at end of file + expect(tools.length).toBe(1); + }); +}); diff --git a/js/src/sdk/base.toolset.ts b/js/src/sdk/base.toolset.ts index 02a85cc5574..0d9fde49b9c 100644 --- a/js/src/sdk/base.toolset.ts +++ b/js/src/sdk/base.toolset.ts @@ -13,7 +13,7 @@ import { ActionRegistry, CreateActionOptions } from "./actionRegistry"; import { getUserDataJson } from "./utils/config"; import { z } from "zod"; type GetListActionsResponse = { - items: any[] + items: any[]; }; const ZExecuteActionParams = z.object({ @@ -22,347 +22,439 @@ const ZExecuteActionParams = z.object({ entityId: z.string(), nlaText: z.string().optional(), connectedAccountId: z.string().optional(), - config: z.object({ - labels: z.array(z.string()).optional(), - }).optional(), + config: z + .object({ + labels: z.array(z.string()).optional(), + }) + .optional(), }); +type TPreProcessor = ({ + action, + toolRequest, +}: { + action: string; + toolRequest: Record; +}) => Record; +type TPostProcessor = ({ + action, + toolResponse, +}: { + action: string; + toolResponse: ActionExecutionResDto; +}) => ActionExecutionResDto; + +const fileProcessor = ({ + action, + toolResponse, +}: { + action: string; + toolResponse: ActionExecutionResDto; +}): ActionExecutionResDto => { + // @ts-expect-error + const isFile = !!toolResponse.data.response_data.file as boolean; + + if (!isFile) { + return toolResponse; + } + + // @ts-expect-error + const fileData = toolResponse.data.response_data.file; + const { name, content } = fileData as { name: string; content: string }; + const file_name_prefix = `${action}_${Date.now()}`; + const filePath = saveFile(file_name_prefix, content); + + // @ts-ignore + delete toolResponse.data.response_data.file; + + return { + error: toolResponse.error, + successfull: toolResponse.successfull, + data: { + ...toolResponse.data, + file_uri_path: filePath, + }, + }; +}; -type TPreProcessor = ({action, toolRequest}: {action: string, toolRequest: Record}) => Record; -type TPostProcessor = ({action, toolResponse}: {action: string, toolResponse: ActionExecutionResDto}) => ActionExecutionResDto; - -const fileProcessor = ({action, toolResponse}:{action: string, toolResponse: ActionExecutionResDto}): ActionExecutionResDto => { - - // @ts-expect-error - const isFile = !!toolResponse.data.response_data.file as boolean; - - if(!isFile) { - return toolResponse; +export class ComposioToolSet { + client: Composio; + apiKey: string; + runtime: string | null; + entityId: string; + workspace: WorkspaceFactory; + workspaceEnv: ExecEnv; + + localActions: IPythonActionDetails["data"] | undefined; + customActionRegistry: ActionRegistry; + + private processors: { + pre?: TPreProcessor; + post?: TPostProcessor; + } = {}; + + constructor( + apiKey: string | null, + baseUrl: string | null = COMPOSIO_BASE_URL, + runtime: string | null = null, + entityId: string = "default", + workspaceConfig: WorkspaceConfig = Workspace.Host() + ) { + const clientApiKey: string | undefined = + apiKey || + getEnvVariable("COMPOSIO_API_KEY") || + (getUserDataJson().api_key as string); + this.apiKey = clientApiKey; + this.client = new Composio( + this.apiKey, + baseUrl || undefined, + runtime as string + ); + this.customActionRegistry = new ActionRegistry(this.client); + this.runtime = runtime; + this.entityId = entityId; + + if (!workspaceConfig.config.composioBaseURL) { + workspaceConfig.config.composioBaseURL = baseUrl; } + if (!workspaceConfig.config.composioAPIKey) { + workspaceConfig.config.composioAPIKey = apiKey; + } + this.workspace = new WorkspaceFactory(workspaceConfig.env, workspaceConfig); + this.workspaceEnv = workspaceConfig.env; - // @ts-expect-error - const fileData = toolResponse.data.response_data.file - const { name, content } = fileData as { name: string, content: string }; - const file_name_prefix = `${action}_${Date.now()}`; - const filePath = saveFile(file_name_prefix, content); - - // @ts-ignore - delete toolResponse.data.response_data.file - - return { - error: toolResponse.error, - successfull: toolResponse.successfull, - data: { - ...toolResponse.data, - file_uri_path: filePath + if (typeof process !== "undefined") { + process.on("exit", async () => { + await this.workspace.workspace?.teardown(); + }); + } + } + + /** + * @deprecated This method is deprecated. Please use this.client.getExpectedParamsForUser instead. + */ + async getExpectedParamsForUser( + params: { + app?: string; + integrationId?: string; + entityId?: string; + authScheme?: + | "OAUTH2" + | "OAUTH1" + | "API_KEY" + | "BASIC" + | "BEARER_TOKEN" + | "BASIC_WITH_JWT"; + } = {} + ) { + return this.client.getExpectedParamsForUser(params); + } + + async setup() { + await this.workspace.new(); + + if (!this.localActions && this.workspaceEnv !== ExecEnv.HOST) { + this.localActions = await ( + this.workspace.workspace as RemoteWorkspace + ).getLocalActionsSchema(); + } + } + + async getActionsSchema( + filters: { actions?: Optional> } = {}, + entityId?: Optional + ): Promise[0]>> { + await this.setup(); + let actions = ( + await this.client.actions.list({ + actions: filters.actions?.join(","), + showAll: true, + }) + ).items; + const localActionsMap = new Map< + string, + NonNullable[0] + >(); + filters.actions?.forEach((action: string) => { + const actionData = this.localActions?.find((a: any) => a.name === action); + if (actionData) { + localActionsMap.set(actionData.name!, actionData); + } + }); + const uniqueLocalActions = Array.from(localActionsMap.values()); + const _newActions = filters.actions?.map((action: string) => + action.toLowerCase() + ); + const toolsWithCustomActions = ( + await this.customActionRegistry.getActions({ actions: _newActions! }) + ).filter((action) => { + if ( + _newActions && + !_newActions.includes(action.parameters.title.toLowerCase()!) + ) { + return false; + } + return true; + }); + + const toolsActions = [ + ...actions!, + ...uniqueLocalActions, + ...toolsWithCustomActions, + ]; + + return toolsActions.map((action) => { + return this.modifyActionForLocalExecution(action); + }); + } + + /** + * @deprecated This method is deprecated. Please use this.client.connectedAccounts.getAuthParams instead. + */ + async getAuthParams(data: { connectedAccountId: string }) { + return this.client.connectedAccounts.getAuthParams({ + connectedAccountId: data.connectedAccountId, + }); + } + + async getTools( + filters: { + apps: Sequence; + tags?: Optional>; + useCase?: Optional; + }, + entityId?: Optional + ): Promise { + throw new Error("Not implemented. Please define in extended toolset"); + } + + async getToolsSchema( + filters: { + actions?: Optional>; + apps?: Array; + tags?: Optional>; + useCase?: Optional; + useCaseLimit?: Optional; + filterByAvailableApps?: Optional; + }, + entityId?: Optional + ): Promise[0]>> { + await this.setup(); + + const apps = await this.client.actions.list({ + ...(filters?.apps && { apps: filters?.apps?.join(",") }), + ...(filters?.tags && { tags: filters?.tags?.join(",") }), + ...(filters?.useCase && { useCase: filters?.useCase }), + ...(filters?.actions && { actions: filters?.actions?.join(",") }), + ...(filters?.useCaseLimit && { usecaseLimit: filters?.useCaseLimit }), + filterByAvailableApps: filters?.filterByAvailableApps ?? undefined, + }); + const localActions = new Map< + string, + NonNullable[0] + >(); + if (filters.apps && Array.isArray(filters.apps)) { + for (const appName of filters.apps!) { + const actionData = this.localActions?.filter( + (a: { appName: string }) => a.appName === appName + ); + if (actionData) { + for (const action of actionData) { + localActions.set(action.name, action); + } } + } } - - -} - -export class ComposioToolSet { - client: Composio; - apiKey: string; - runtime: string | null; - entityId: string; - workspace: WorkspaceFactory; - workspaceEnv: ExecEnv; - - localActions: IPythonActionDetails["data"] | undefined; - customActionRegistry: ActionRegistry; - - private processors: { - pre?: TPreProcessor; - post?: TPostProcessor; - } = {}; - - constructor( - apiKey: string | null, - baseUrl: string | null = COMPOSIO_BASE_URL, - runtime: string | null = null, - entityId: string = "default", - workspaceConfig: WorkspaceConfig = Workspace.Host() - ) { - const clientApiKey: string | undefined = apiKey || getEnvVariable("COMPOSIO_API_KEY") || getUserDataJson().api_key as string; - this.apiKey = clientApiKey; - this.client = new Composio(this.apiKey, baseUrl || undefined, runtime as string); - this.customActionRegistry = new ActionRegistry(this.client); - this.runtime = runtime; - this.entityId = entityId; - - if (!workspaceConfig.config.composioBaseURL) { - workspaceConfig.config.composioBaseURL = baseUrl + const uniqueLocalActions = Array.from(localActions.values()); + + const toolsWithCustomActions = ( + await this.customActionRegistry.getAllActions() + ) + .filter((action) => { + if ( + filters.actions && + !filters.actions.some( + (actionName) => + actionName.toLowerCase() === + action.metadata.actionName!.toLowerCase() + ) + ) { + return false; } - if (!workspaceConfig.config.composioAPIKey) { - workspaceConfig.config.composioAPIKey = apiKey; + if ( + filters.apps && + !filters.apps.some( + (appName) => + appName.toLowerCase() === action.metadata.toolName!.toLowerCase() + ) + ) { + return false; } - this.workspace = new WorkspaceFactory(workspaceConfig.env, workspaceConfig); - this.workspaceEnv = workspaceConfig.env; - - if (typeof process !== 'undefined') { - process.on("exit", async () => { - await this.workspace.workspace?.teardown(); - }); + if ( + filters.tags && + !filters.tags.some( + (tag) => tag.toLocaleLowerCase() === "custom".toLocaleLowerCase() + ) + ) { + return false; } - + return true; + }) + .map((action) => { + return action.schema; + }); + + const toolsActions = [ + ...apps?.items!, + ...uniqueLocalActions, + ...toolsWithCustomActions, + ]; + + return toolsActions.map((action) => { + return this.modifyActionForLocalExecution(action); + }); + } + + modifyActionForLocalExecution(toolSchema: any) { + const properties = convertReqParams(toolSchema.parameters.properties); + toolSchema.parameters.properties = properties; + const response = toolSchema.response.properties; + + for (const responseKey of Object.keys(response)) { + if (responseKey === "file") { + response["file_uri_path"] = { + type: "string", + title: "Name", + description: + "Local absolute path to the file or http url to the file", + }; + + delete response[responseKey]; + } } - /** - * @deprecated This method is deprecated. Please use this.client.getExpectedParamsForUser instead. - */ - async getExpectedParamsForUser( - params: { app?: string; integrationId?: string; entityId?: string; authScheme?: "OAUTH2" | "OAUTH1" | "API_KEY" | "BASIC" | "BEARER_TOKEN" | "BASIC_WITH_JWT" } = {}, - ) { - return this.client.getExpectedParamsForUser(params); + return toolSchema; + } + + async createAction(options: CreateActionOptions) { + return this.customActionRegistry.createAction(options); + } + + private isCustomAction(action: string) { + return this.customActionRegistry + .getActions({ actions: [action] }) + .then((actions) => actions.length > 0); + } + + async executeAction(functionParams: z.infer) { + const { + action, + params: inputParams = {}, + entityId = "default", + nlaText = "", + connectedAccountId, + } = ZExecuteActionParams.parse(functionParams); + let params = inputParams; + + const isPreProcessorAndIsFunction = + typeof this?.processors?.pre === "function"; + if (isPreProcessorAndIsFunction && this.processors.pre) { + params = this.processors.pre({ + action: action, + toolRequest: params, + }); } - - async setup() { - await this.workspace.new(); - - if (!this.localActions && this.workspaceEnv !== ExecEnv.HOST) { - this.localActions = await (this.workspace.workspace as RemoteWorkspace).getLocalActionsSchema(); - } - } - - async getActionsSchema( - filters: { actions?: Optional> } = {}, - entityId?: Optional - ): Promise[0]>> { - await this.setup(); - let actions = (await this.client.actions.list({ - actions: filters.actions?.join(","), - showAll: true - })).items; - const localActionsMap = new Map[0]>(); - filters.actions?.forEach((action: string) => { - const actionData = this.localActions?.find((a: any) => a.name === action); - if (actionData) { - localActionsMap.set(actionData.name!, actionData); - } - }); - const uniqueLocalActions = Array.from(localActionsMap.values()); - const _newActions = filters.actions?.map((action: string) => action.toLowerCase()); - const toolsWithCustomActions = (await this.customActionRegistry.getActions({ actions: _newActions! })).filter((action) => { - if (_newActions && !_newActions.includes(action.parameters.title.toLowerCase()!)) { - return false; - } - return true; + // Custom actions are always executed in the host/local environment for JS SDK + if (await this.isCustomAction(action)) { + let accountId = connectedAccountId; + if (!accountId) { + // fetch connected account id + const connectedAccounts = await this.client.connectedAccounts.list({ + user_uuid: entityId, }); + accountId = connectedAccounts?.items[0]?.id; + } - const toolsActions = [...actions!, ...uniqueLocalActions, ...toolsWithCustomActions]; + if (!accountId) { + throw new Error("No connected account found for the user"); + } - return toolsActions.map((action) => { - return this.modifyActionForLocalExecution(action); - }); + return this.customActionRegistry.executeAction(action, params, { + entityId: entityId, + connectionId: accountId, + }); } - - /** - * @deprecated This method is deprecated. Please use this.client.connectedAccounts.getAuthParams instead. - */ - async getAuthParams(data: { connectedAccountId: string }) { - return this.client.connectedAccounts.getAuthParams({ - connectedAccountId: data.connectedAccountId - }); + if (this.workspaceEnv && this.workspaceEnv !== ExecEnv.HOST) { + const workspace = await this.workspace.get(); + return workspace.executeAction(action, params, { + entityId: this.entityId, + }); } - - async getTools( - filters: { - apps: Sequence; - tags?: Optional>; - useCase?: Optional; - }, - entityId?: Optional - ): Promise { - throw new Error("Not implemented. Please define in extended toolset"); + const convertedParams = await converReqParamForActionExecution(params); + const data = (await this.client + .getEntity(entityId) + .execute({ + actionName: action, + params: convertedParams, + text: nlaText, + })) as ActionExecutionResDto; + + return this.processResponse(data, { + action: action, + entityId: entityId, + }); + } + + private async processResponse( + data: ActionExecutionResDto, + meta: { + action: string; + entityId: string; } - - async getToolsSchema( - filters: { - actions?: Optional>; - apps?: Array; - tags?: Optional>; - useCase?: Optional; - useCaseLimit?: Optional; - filterByAvailableApps?: Optional; - }, - entityId?: Optional - ): Promise[0]>> { - await this.setup(); - - const apps = await this.client.actions.list({ - ...(filters?.apps && { apps: filters?.apps?.join(",") }), - ...(filters?.tags && { tags: filters?.tags?.join(",") }), - ...(filters?.useCase && { useCase: filters?.useCase }), - ...(filters?.actions && { actions: filters?.actions?.join(",") }), - ...(filters?.useCaseLimit && { usecaseLimit: filters?.useCaseLimit }), - filterByAvailableApps: filters?.filterByAvailableApps ?? undefined - }); - const localActions = new Map[0]>(); - if (filters.apps && Array.isArray(filters.apps)) { - for (const appName of filters.apps!) { - const actionData = this.localActions?.filter((a: { appName: string }) => a.appName === appName); - if (actionData) { - for (const action of actionData) { - localActions.set(action.name, action); - } - } - } - } - const uniqueLocalActions = Array.from(localActions.values()); - - const toolsWithCustomActions = (await this.customActionRegistry.getAllActions()).filter((action) => { - if (filters.actions && !filters.actions.some(actionName => actionName.toLowerCase() === action.metadata.actionName!.toLowerCase())) { - return false; - } - if (filters.apps && !filters.apps.some(appName => appName.toLowerCase() === action.metadata.toolName!.toLowerCase())) { - return false; - } - if (filters.tags && !filters.tags.some(tag => tag.toLocaleLowerCase() === "custom".toLocaleLowerCase())) { - return false; - } - return true; - }).map((action) => { - return action.schema; - }); - - const toolsActions = [...apps?.items!, ...uniqueLocalActions, ...toolsWithCustomActions]; - - return toolsActions.map((action) => { - return this.modifyActionForLocalExecution(action); - }); - - } - - modifyActionForLocalExecution(toolSchema: any) { - const properties = convertReqParams(toolSchema.parameters.properties); - toolSchema.parameters.properties = properties; - const response = toolSchema.response.properties; - - for (const responseKey of Object.keys(response)) { - if (responseKey === "file") { - response["file_uri_path"] = { - type: "string", - title: "Name", - description: "Local absolute path to the file or http url to the file" - } - - delete response[responseKey]; - } - } - - return toolSchema; - } - - async createAction(options: CreateActionOptions) { - return this.customActionRegistry.createAction(options); - } - - private isCustomAction(action: string) { - return this.customActionRegistry.getActions({ actions: [action] }).then((actions) => actions.length > 0); - } - - async executeAction(functionParams: z.infer) { - - const {action, params:inputParams={}, entityId="default", nlaText="", connectedAccountId} = ZExecuteActionParams.parse(functionParams); - let params = inputParams; - - const isPreProcessorAndIsFunction = typeof this?.processors?.pre === "function"; - if(isPreProcessorAndIsFunction && this.processors.pre) { - params = this.processors.pre({ - action: action, - toolRequest: params - }); - } - // Custom actions are always executed in the host/local environment for JS SDK - if (await this.isCustomAction(action)) { - let accountId = connectedAccountId; - if (!accountId) { - // fetch connected account id - const connectedAccounts = await this.client.connectedAccounts.list({ - user_uuid: entityId - }); - accountId = connectedAccounts?.items[0]?.id; - } - - if(!accountId) { - throw new Error("No connected account found for the user"); - } - - return this.customActionRegistry.executeAction(action, params, { - entityId: entityId, - connectionId: accountId - }); - } - if (this.workspaceEnv && this.workspaceEnv !== ExecEnv.HOST) { - const workspace = await this.workspace.get(); - return workspace.executeAction(action, params, { - entityId: this.entityId - }); - } - const convertedParams = await converReqParamForActionExecution(params); - const data = await this.client.getEntity(entityId).execute({actionName: action, params: convertedParams, text: nlaText}) as ActionExecutionResDto; - - - return this.processResponse(data, { - action: action, - entityId: entityId - }); + ): Promise { + let dataToReturn = { ...data }; + // @ts-ignore + const isFile = !!data?.response_data?.file; + if (isFile) { + dataToReturn = fileProcessor({ + action: meta.action, + toolResponse: dataToReturn, + }) as ActionExecutionResDto; } - private async processResponse( - data: ActionExecutionResDto, - meta: { - action: string, - entityId: string - } - ): Promise { - let dataToReturn = {...data}; - // @ts-ignore - const isFile = !!data?.response_data?.file; - if (isFile) { - dataToReturn = fileProcessor({ - action: meta.action, - toolResponse: dataToReturn - }) as ActionExecutionResDto; - } - - const isPostProcessorAndIsFunction = !!this.processors.post && typeof this.processors.post === "function"; - if (isPostProcessorAndIsFunction && this.processors.post) { - dataToReturn = this.processors.post({ - action: meta.action, - toolResponse: dataToReturn - }); - } - - return dataToReturn; + const isPostProcessorAndIsFunction = + !!this.processors.post && typeof this.processors.post === "function"; + if (isPostProcessorAndIsFunction && this.processors.post) { + dataToReturn = this.processors.post({ + action: meta.action, + toolResponse: dataToReturn, + }); } + return dataToReturn; + } - async addPreProcessor(processor: TPreProcessor) { - if(typeof processor === "function") { - this.processors.pre = processor as TPreProcessor; - } - else { - throw new Error("Invalid processor type"); - } - } - - async addPostProcessor(processor: TPostProcessor) { - if(typeof processor === "function") { - this.processors.post = processor as TPostProcessor; - } - else { - throw new Error("Invalid processor type"); - } + async addPreProcessor(processor: TPreProcessor) { + if (typeof processor === "function") { + this.processors.pre = processor as TPreProcessor; + } else { + throw new Error("Invalid processor type"); } + } - async removePreProcessor() { - delete this.processors.pre; + async addPostProcessor(processor: TPostProcessor) { + if (typeof processor === "function") { + this.processors.post = processor as TPostProcessor; + } else { + throw new Error("Invalid processor type"); } + } - async removePostProcessor() { - delete this.processors.post; - } + async removePreProcessor() { + delete this.processors.pre; + } -} \ No newline at end of file + async removePostProcessor() { + delete this.processors.post; + } +} diff --git a/js/src/sdk/index.spec.ts b/js/src/sdk/index.spec.ts index 21366166b5b..a3537524165 100644 --- a/js/src/sdk/index.spec.ts +++ b/js/src/sdk/index.spec.ts @@ -2,132 +2,138 @@ import { describe, it, expect } from "@jest/globals"; import { Composio } from "./index"; import { getTestConfig } from "../../config/getTestConfig"; import { client as axiosClient } from "./client/services.gen"; -import { BASE_ERROR_CODE_INFO, SDK_ERROR_CODES } from "./utils/errors/src/constants"; +import { + BASE_ERROR_CODE_INFO, + SDK_ERROR_CODES, +} from "./utils/errors/src/constants"; import AxiosMockAdapter from "axios-mock-adapter"; const { COMPOSIO_API_KEY, BACKEND_HERMES_URL } = getTestConfig(); describe("Basic SDK spec suite", () => { - it("should create a basic client", () => { - const client = new Composio(COMPOSIO_API_KEY); - expect(client).toBeInstanceOf(Composio); - }); - - it("should throw an error if apiKey is not provided", async () => { - const originalExit = process.exit; - - // @ts-expect-error - process.exit = jest.fn(); - expect(() => new Composio()).toThrow('🔑 API Key is not provided'); - process.exit = originalExit; - - }); - - it("should handle 404 error gracefully", async () => { - const client = new Composio(COMPOSIO_API_KEY); - const mock = new AxiosMockAdapter(axiosClient.instance); - mock.onGet("/api/v1/apps").reply(404, { detail: "Not found" }); - - try { - await client.apps.list(); - } catch (e: any) { - const errorCode = SDK_ERROR_CODES.BACKEND.NOT_FOUND; - const errorInfo = BASE_ERROR_CODE_INFO[errorCode]; - expect(e.errCode).toBe(errorCode); - expect(e.message).toContain(errorInfo.message); - expect(e.description).toBe(errorInfo.description); - expect(e.errorId).toBeDefined(); - expect(e.name).toBe("ComposioError"); - expect(e.possibleFix).toBe(errorInfo.possibleFix); - } - - mock.reset(); - }); - - it("should handle 400 error gracefully", async () => { - const client = new Composio(COMPOSIO_API_KEY); - const mock = new AxiosMockAdapter(axiosClient.instance); - mock.onGet("/api/v1/apps").reply(400, { errors: ["Invalid request for apps"] }); - - try { - await client.apps.list(); - } catch (e: any) { - const errorCode = SDK_ERROR_CODES.BACKEND.BAD_REQUEST; - const errorInfo = BASE_ERROR_CODE_INFO[errorCode]; - expect(e.errCode).toBe(errorCode); - expect(e.message).toContain("Validation Errors while making request to https://backend.composio.dev/api/v1/apps"); - expect(e.description).toContain("Invalid request for apps"); - } - - mock.reset(); - }); - - it("should handle 500 and 502 error gracefully", async () => { - const client = new Composio(COMPOSIO_API_KEY); - const mock = new AxiosMockAdapter(axiosClient.instance); - mock.onGet("/api/v1/apps").reply(500, { detail: "Internal Server Error" }); - - try { - await client.apps.list(); - } catch (e: any) { - const errorCode = SDK_ERROR_CODES.BACKEND.SERVER_ERROR; - const errorInfo = BASE_ERROR_CODE_INFO[errorCode]; - expect(e.errCode).toBe(errorCode); - expect(e.message).toContain(errorInfo.message); - expect(e.description).toContain(errorInfo.description); - expect(e.errorId).toBeDefined(); - expect(e.name).toBe("ComposioError"); - expect(e.possibleFix).toContain(errorInfo.possibleFix); - } - - mock.onGet("/api/v1/apps").reply(502, { detail: "Bad Gateway" }); - - try { - const apps = await client.apps.list(); - } catch (e: any) { - const errorCode = SDK_ERROR_CODES.BACKEND.SERVER_UNAVAILABLE; - const errorInfo = BASE_ERROR_CODE_INFO[errorCode]; - expect(e.errCode).toBe(errorCode); - expect(e.message).toContain(errorInfo.message); - expect(e.description).toContain(errorInfo.description); - expect(e.errorId).toBeDefined(); - expect(e.name).toBe("ComposioError"); - expect(e.possibleFix).toContain(errorInfo.possibleFix); - } - - mock.reset(); - }); - - it("should give request timeout error", async () => { - const client = new Composio(COMPOSIO_API_KEY); - const mock = new AxiosMockAdapter(axiosClient.instance); - mock.onGet("/api/v1/apps").reply(408, {}); - - try { - await client.apps.list(); - } catch (e: any) { - const errorCode = SDK_ERROR_CODES.COMMON.REQUEST_TIMEOUT; - const errorInfo = BASE_ERROR_CODE_INFO[errorCode]; - expect(e.errCode).toBe(errorCode); - expect(e.message).toContain(errorInfo.message); - expect(e.description).toBe(errorInfo.description); - expect(e.possibleFix).toBe(errorInfo.possibleFix); - } - - mock.reset(); - }); - - it("syntax error handling", () => { - expect(() => new Composio()).toThrow('🔑 API Key is not provided'); - }); - - it("should get an entity and then fetch a connection", async () => { - const app = "github"; - const composio = new Composio(COMPOSIO_API_KEY, BACKEND_HERMES_URL); - const entity = composio.getEntity("default"); - - expect(entity.id).toBe("default"); - - const connection = await entity.getConnection({app: app!}); - expect(connection.appUniqueId).toBe(app); - }); + it("should create a basic client", () => { + const client = new Composio(COMPOSIO_API_KEY); + expect(client).toBeInstanceOf(Composio); + }); + + it("should throw an error if apiKey is not provided", async () => { + const originalExit = process.exit; + + // @ts-expect-error + process.exit = jest.fn(); + expect(() => new Composio()).toThrow("🔑 API Key is not provided"); + process.exit = originalExit; + }); + + it("should handle 404 error gracefully", async () => { + const client = new Composio(COMPOSIO_API_KEY); + const mock = new AxiosMockAdapter(axiosClient.instance); + mock.onGet("/api/v1/apps").reply(404, { detail: "Not found" }); + + try { + await client.apps.list(); + } catch (e: any) { + const errorCode = SDK_ERROR_CODES.BACKEND.NOT_FOUND; + const errorInfo = BASE_ERROR_CODE_INFO[errorCode]; + expect(e.errCode).toBe(errorCode); + expect(e.message).toContain(errorInfo.message); + expect(e.description).toBe(errorInfo.description); + expect(e.errorId).toBeDefined(); + expect(e.name).toBe("ComposioError"); + expect(e.possibleFix).toBe(errorInfo.possibleFix); + } + + mock.reset(); + }); + + it("should handle 400 error gracefully", async () => { + const client = new Composio(COMPOSIO_API_KEY); + const mock = new AxiosMockAdapter(axiosClient.instance); + mock + .onGet("/api/v1/apps") + .reply(400, { errors: ["Invalid request for apps"] }); + + try { + await client.apps.list(); + } catch (e: any) { + const errorCode = SDK_ERROR_CODES.BACKEND.BAD_REQUEST; + const errorInfo = BASE_ERROR_CODE_INFO[errorCode]; + expect(e.errCode).toBe(errorCode); + expect(e.message).toContain( + "Validation Errors while making request to https://backend.composio.dev/api/v1/apps" + ); + expect(e.description).toContain("Invalid request for apps"); + } + + mock.reset(); + }); + + it("should handle 500 and 502 error gracefully", async () => { + const client = new Composio(COMPOSIO_API_KEY); + const mock = new AxiosMockAdapter(axiosClient.instance); + mock.onGet("/api/v1/apps").reply(500, { detail: "Internal Server Error" }); + + try { + await client.apps.list(); + } catch (e: any) { + const errorCode = SDK_ERROR_CODES.BACKEND.SERVER_ERROR; + const errorInfo = BASE_ERROR_CODE_INFO[errorCode]; + expect(e.errCode).toBe(errorCode); + expect(e.message).toContain(errorInfo.message); + expect(e.description).toContain(errorInfo.description); + expect(e.errorId).toBeDefined(); + expect(e.name).toBe("ComposioError"); + expect(e.possibleFix).toContain(errorInfo.possibleFix); + } + + mock.onGet("/api/v1/apps").reply(502, { detail: "Bad Gateway" }); + + try { + const apps = await client.apps.list(); + } catch (e: any) { + const errorCode = SDK_ERROR_CODES.BACKEND.SERVER_UNAVAILABLE; + const errorInfo = BASE_ERROR_CODE_INFO[errorCode]; + expect(e.errCode).toBe(errorCode); + expect(e.message).toContain(errorInfo.message); + expect(e.description).toContain(errorInfo.description); + expect(e.errorId).toBeDefined(); + expect(e.name).toBe("ComposioError"); + expect(e.possibleFix).toContain(errorInfo.possibleFix); + } + + mock.reset(); + }); + + it("should give request timeout error", async () => { + const client = new Composio(COMPOSIO_API_KEY); + const mock = new AxiosMockAdapter(axiosClient.instance); + mock.onGet("/api/v1/apps").reply(408, {}); + + try { + await client.apps.list(); + } catch (e: any) { + const errorCode = SDK_ERROR_CODES.COMMON.REQUEST_TIMEOUT; + const errorInfo = BASE_ERROR_CODE_INFO[errorCode]; + expect(e.errCode).toBe(errorCode); + expect(e.message).toContain(errorInfo.message); + expect(e.description).toBe(errorInfo.description); + expect(e.possibleFix).toBe(errorInfo.possibleFix); + } + + mock.reset(); + }); + + it("syntax error handling", () => { + expect(() => new Composio()).toThrow("🔑 API Key is not provided"); + }); + + it("should get an entity and then fetch a connection", async () => { + const app = "github"; + const composio = new Composio(COMPOSIO_API_KEY, BACKEND_HERMES_URL); + const entity = composio.getEntity("default"); + + expect(entity.id).toBe("default"); + + const connection = await entity.getConnection({ app: app! }); + expect(connection.appUniqueId).toBe(app); + }); }); diff --git a/js/src/sdk/index.ts b/js/src/sdk/index.ts index 7e67dda8071..2bc67f496dd 100644 --- a/js/src/sdk/index.ts +++ b/js/src/sdk/index.ts @@ -1,218 +1,299 @@ -import { ConnectedAccounts } from './models/connectedAccounts'; -import { Apps } from './models/apps'; -import { Actions } from './models/actions'; -import { Triggers } from './models/triggers'; -import { Integrations } from './models/integrations'; -import { ActiveTriggers } from './models/activeTriggers'; -import { BackendClient } from './models/backendClient'; -import { Entity } from './models/Entity'; -import axios from 'axios'; -import { getPackageJsonDir } from './utils/projectUtils'; -import { isNewerVersion } from './utils/other'; -import { CEG } from './utils/error'; -import { GetConnectorInfoResDTO } from './client'; -import logger, { getLogLevel } from '../utils/logger'; -import { SDK_ERROR_CODES } from './utils/errors/src/constants'; -import { getSDKConfig } from './utils/config'; -import ComposioSDKContext from './utils/composioContext'; -import { TELEMETRY_LOGGER } from './utils/telemetry'; -import { TELEMETRY_EVENTS } from './utils/telemetry/events'; +import { ConnectedAccounts } from "./models/connectedAccounts"; +import { Apps } from "./models/apps"; +import { Actions } from "./models/actions"; +import { Triggers } from "./models/triggers"; +import { Integrations } from "./models/integrations"; +import { ActiveTriggers } from "./models/activeTriggers"; +import { BackendClient } from "./models/backendClient"; +import { Entity } from "./models/Entity"; +import axios from "axios"; +import { getPackageJsonDir } from "./utils/projectUtils"; +import { isNewerVersion } from "./utils/other"; +import { CEG } from "./utils/error"; +import { GetConnectorInfoResDTO } from "./client"; +import logger, { getLogLevel } from "../utils/logger"; +import { SDK_ERROR_CODES } from "./utils/errors/src/constants"; +import { getSDKConfig } from "./utils/config"; +import ComposioSDKContext from "./utils/composioContext"; +import { TELEMETRY_LOGGER } from "./utils/telemetry"; +import { TELEMETRY_EVENTS } from "./utils/telemetry/events"; export class Composio { - /** - * The Composio class serves as the main entry point for interacting with the Composio SDK. - * It provides access to various models that allow for operations on connected accounts, apps, - * actions, triggers, integrations, and active triggers. - */ - backendClient: BackendClient; - connectedAccounts: ConnectedAccounts; - apps: Apps; - actions: Actions; - triggers: Triggers; - integrations: Integrations; - activeTriggers: ActiveTriggers; - - /** - * Initializes a new instance of the Composio class. - * - * @param {string} [apiKey] - The API key for authenticating with the Composio backend. Can also be set locally in an environment variable. - * @param {string} [baseUrl] - The base URL for the Composio backend. By default, it is set to the production URL. - * @param {string} [runtime] - The runtime environment for the SDK. - */ - constructor(apiKey?: string, baseUrl?: string, runtime?: string) { - - // // Parse the base URL and API key, falling back to environment variables or defaults if not provided. - const { baseURL: baseURLParsed, apiKey: apiKeyParsed } = getSDKConfig(baseUrl, apiKey); - const loggingLevel = getLogLevel(); - - ComposioSDKContext.apiKey = apiKeyParsed; - ComposioSDKContext.baseURL = baseURLParsed; - ComposioSDKContext.frameworkRuntime = runtime; - ComposioSDKContext.composioVersion = require(getPackageJsonDir() + '/package.json').version; - - TELEMETRY_LOGGER.manualTelemetry(TELEMETRY_EVENTS.SDK_INITIALIZED, {}); - if(!apiKeyParsed){ - throw CEG.getCustomError(SDK_ERROR_CODES.COMMON.API_KEY_UNAVAILABLE,{ - message: "🔑 API Key is not provided", - description: "You need to provide it in the constructor or as an environment variable COMPOSIO_API_KEY", - possibleFix: "Please provide a valid API Key. You can get it from https://app.composio.dev/settings" - }); - } + /** + * The Composio class serves as the main entry point for interacting with the Composio SDK. + * It provides access to various models that allow for operations on connected accounts, apps, + * actions, triggers, integrations, and active triggers. + */ + backendClient: BackendClient; + connectedAccounts: ConnectedAccounts; + apps: Apps; + actions: Actions; + triggers: Triggers; + integrations: Integrations; + activeTriggers: ActiveTriggers; + /** + * Initializes a new instance of the Composio class. + * + * @param {string} [apiKey] - The API key for authenticating with the Composio backend. Can also be set locally in an environment variable. + * @param {string} [baseUrl] - The base URL for the Composio backend. By default, it is set to the production URL. + * @param {string} [runtime] - The runtime environment for the SDK. + */ + constructor(apiKey?: string, baseUrl?: string, runtime?: string) { + // // Parse the base URL and API key, falling back to environment variables or defaults if not provided. + const { baseURL: baseURLParsed, apiKey: apiKeyParsed } = getSDKConfig( + baseUrl, + apiKey + ); + const loggingLevel = getLogLevel(); - logger.info(`Initializing Composio w API Key: [REDACTED] and baseURL: ${baseURLParsed}, Log level: ${loggingLevel.toUpperCase()}`); + ComposioSDKContext.apiKey = apiKeyParsed; + ComposioSDKContext.baseURL = baseURLParsed; + ComposioSDKContext.frameworkRuntime = runtime; + ComposioSDKContext.composioVersion = require( + getPackageJsonDir() + "/package.json" + ).version; - // Initialize the BackendClient with the parsed API key and base URL. - this.backendClient = new BackendClient(apiKeyParsed, baseURLParsed, runtime); + TELEMETRY_LOGGER.manualTelemetry(TELEMETRY_EVENTS.SDK_INITIALIZED, {}); + if (!apiKeyParsed) { + throw CEG.getCustomError(SDK_ERROR_CODES.COMMON.API_KEY_UNAVAILABLE, { + message: "🔑 API Key is not provided", + description: + "You need to provide it in the constructor or as an environment variable COMPOSIO_API_KEY", + possibleFix: + "Please provide a valid API Key. You can get it from https://app.composio.dev/settings", + }); + } - // Instantiate models with dependencies as needed. - this.connectedAccounts = new ConnectedAccounts(this.backendClient); - this.triggers = new Triggers(this.backendClient); - this.apps = new Apps(this.backendClient); - this.actions = new Actions(this.backendClient); - this.integrations = new Integrations(this.backendClient); - this.activeTriggers = new ActiveTriggers(this.backendClient); + logger.info( + `Initializing Composio w API Key: [REDACTED] and baseURL: ${baseURLParsed}, Log level: ${loggingLevel.toUpperCase()}` + ); - this.checkForLatestVersionFromNPM(); - } + // Initialize the BackendClient with the parsed API key and base URL. + this.backendClient = new BackendClient( + apiKeyParsed, + baseURLParsed, + runtime + ); - /** - * Checks for the latest version of the Composio SDK from NPM. - * If a newer version is available, it logs a warning to the console. - */ - async checkForLatestVersionFromNPM() { - try { - const packageName = "composio-core"; - const packageJsonDir = getPackageJsonDir(); - const currentVersionFromPackageJson = require(packageJsonDir + '/package.json').version; - - const response = await axios.get(`https://registry.npmjs.org/${packageName}/latest`); - const latestVersion = response.data.version; - - - if (isNewerVersion(latestVersion, currentVersionFromPackageJson)) { - console.warn(`🚀 Upgrade available! Your composio-core version (${currentVersionFromPackageJson}) is behind. Latest version: ${latestVersion}.`); - } - } catch (error) { - // Ignore and do nothing - } + // Instantiate models with dependencies as needed. + this.connectedAccounts = new ConnectedAccounts(this.backendClient); + this.triggers = new Triggers(this.backendClient); + this.apps = new Apps(this.backendClient); + this.actions = new Actions(this.backendClient); + this.integrations = new Integrations(this.backendClient); + this.activeTriggers = new ActiveTriggers(this.backendClient); + + this.checkForLatestVersionFromNPM(); + } + + /** + * Checks for the latest version of the Composio SDK from NPM. + * If a newer version is available, it logs a warning to the console. + */ + async checkForLatestVersionFromNPM() { + try { + const packageName = "composio-core"; + const packageJsonDir = getPackageJsonDir(); + const currentVersionFromPackageJson = require( + packageJsonDir + "/package.json" + ).version; + + const response = await axios.get( + `https://registry.npmjs.org/${packageName}/latest` + ); + const latestVersion = response.data.version; + + if (isNewerVersion(latestVersion, currentVersionFromPackageJson)) { + console.warn( + `🚀 Upgrade available! Your composio-core version (${currentVersionFromPackageJson}) is behind. Latest version: ${latestVersion}.` + ); + } + } catch (error) { + // Ignore and do nothing } - - - /** - * Retrieves an Entity instance associated with a given ID. - * - * @param {string} [id='default'] - The ID of the entity to retrieve. - * @returns {Entity} An instance of the Entity class. - */ - getEntity(id: string = 'default'): Entity { - return new Entity(this.backendClient, id); + } + + /** + * Retrieves an Entity instance associated with a given ID. + * + * @param {string} [id='default'] - The ID of the entity to retrieve. + * @returns {Entity} An instance of the Entity class. + */ + getEntity(id: string = "default"): Entity { + return new Entity(this.backendClient, id); + } + + async getExpectedParamsForUser( + params: { + app?: string; + integrationId?: string; + entityId?: string; + authScheme?: + | "OAUTH2" + | "OAUTH1" + | "API_KEY" + | "BASIC" + | "BEARER_TOKEN" + | "BASIC_WITH_JWT"; + } = {} + ): Promise<{ + expectedInputFields: GetConnectorInfoResDTO["expectedInputFields"]; + integrationId: string; + authScheme: + | "OAUTH2" + | "OAUTH1" + | "API_KEY" + | "BASIC" + | "BEARER_TOKEN" + | "BASIC_WITH_JWT"; + }> { + const { app } = params; + let { integrationId } = params; + if (integrationId === null && app === null) { + throw new Error("Both `integration_id` and `app` cannot be None"); } - async getExpectedParamsForUser( - params: { app?: string; integrationId?: string; entityId?: string; authScheme?: "OAUTH2" | "OAUTH1" | "API_KEY" | "BASIC" | "BEARER_TOKEN" | "BASIC_WITH_JWT" } = {}, - ): Promise<{ expectedInputFields: GetConnectorInfoResDTO["expectedInputFields"], integrationId: string, authScheme: "OAUTH2" | "OAUTH1" | "API_KEY" | "BASIC" | "BEARER_TOKEN" | "BASIC_WITH_JWT" }> { - const { app } = params; - let { integrationId } = params; - if (integrationId === null && app === null) { - throw new Error( - "Both `integration_id` and `app` cannot be None" - ); + if (!integrationId) { + try { + const integrations = await this.integrations.list({ + appName: app!, + showDisabled: false, + }); + if (params.authScheme && integrations) { + integrations.items = integrations.items.filter( + (integration: any) => integration.authScheme === params.authScheme + ); } + integrationId = (integrations?.items[0] as any)?.id; + } catch (_) { + // do nothing + } + } - if (!integrationId) { - try { - const integrations = await this.integrations.list({ - appName: app!, - showDisabled: false - }) - if (params.authScheme && integrations) { - integrations.items = integrations.items.filter((integration: any) => integration.authScheme === params.authScheme); - } - integrationId = (integrations?.items[0] as any)?.id; - } catch (_) { - // do nothing - } - } + let integration = integrationId + ? await this.integrations.get({ + integrationId: integrationId!, + }) + : undefined; + + if (integration) { + return { + expectedInputFields: integration.expectedInputFields, + integrationId: integration.id!, + authScheme: integration.authScheme as + | "OAUTH2" + | "OAUTH1" + | "API_KEY" + | "BASIC" + | "BEARER_TOKEN" + | "BASIC_WITH_JWT", + }; + } - let integration = integrationId ? (await this.integrations.get({ - integrationId: integrationId! - })) : undefined; + const appInfo = await this.apps.get({ + appKey: app!.toLocaleLowerCase(), + }); - if(integration) { - return { - expectedInputFields: integration.expectedInputFields, - integrationId: integration.id!, - authScheme: integration.authScheme as "OAUTH2" | "OAUTH1" | "API_KEY" | "BASIC" | "BEARER_TOKEN" | "BASIC_WITH_JWT" - } - } + const preferredAuthScheme = [ + "OAUTH2", + "OAUTH1", + "API_KEY", + "BASIC", + "BEARER_TOKEN", + "BASIC_WITH_JWT", + ]; - const appInfo = await this.apps.get({ - appKey: app!.toLocaleLowerCase() - }); + let schema: (typeof preferredAuthScheme)[number] | undefined = + params.authScheme; - const preferredAuthScheme = ["OAUTH2", "OAUTH1", "API_KEY", "BASIC", "BEARER_TOKEN", "BASIC_WITH_JWT"]; - - let schema: typeof preferredAuthScheme[number] | undefined = params.authScheme; - - if(!schema) { - for(const scheme of preferredAuthScheme) { - if(appInfo.auth_schemes?.map((_authScheme: any) => _authScheme.mode).includes(scheme)) { - schema = scheme; - break; - } - } + if (!schema) { + for (const scheme of preferredAuthScheme) { + if ( + appInfo.auth_schemes + ?.map((_authScheme: any) => _authScheme.mode) + .includes(scheme) + ) { + schema = scheme; + break; } + } + } - const areNoFieldsRequiredForIntegration = (appInfo.testConnectors?.length ?? 0) > 0 || ((appInfo.auth_schemes?.find((_authScheme: any) => _authScheme.mode === schema) as any)?.fields?.filter((field: any) => !field.expected_from_customer)?.length ?? 0) == 0; + const areNoFieldsRequiredForIntegration = + (appInfo.testConnectors?.length ?? 0) > 0 || + (( + appInfo.auth_schemes?.find( + (_authScheme: any) => _authScheme.mode === schema + ) as any + )?.fields?.filter((field: any) => !field.expected_from_customer) + ?.length ?? 0) == 0; - if (!areNoFieldsRequiredForIntegration) { - throw new Error( - `No default credentials available for this app, please create new integration by going to app.composio.dev or through CLI - composio add ${appInfo.key}` - ); - } + if (!areNoFieldsRequiredForIntegration) { + throw new Error( + `No default credentials available for this app, please create new integration by going to app.composio.dev or through CLI - composio add ${appInfo.key}` + ); + } - const timestamp = new Date().toISOString().replace(/[-:.]/g, ""); - const hasRelevantTestConnectors = params.authScheme ? appInfo.testConnectors?.filter((connector: any) => connector.authScheme === params.authScheme)?.length! > 0 : appInfo.testConnectors?.length! > 0; - if(hasRelevantTestConnectors) { - integration = await this.integrations.create({ - appId: appInfo.appId, - name: `integration_${timestamp}`, - authScheme: schema, - authConfig: {}, - useComposioAuth: true, - }); - - return { - expectedInputFields: integration?.expectedInputFields!, - integrationId: integration?.id!, - authScheme: integration?.authScheme as "OAUTH2" | "OAUTH1" | "API_KEY" | "BASIC" | "BEARER_TOKEN" | "BASIC_WITH_JWT" - } - } + const timestamp = new Date().toISOString().replace(/[-:.]/g, ""); + const hasRelevantTestConnectors = params.authScheme + ? appInfo.testConnectors?.filter( + (connector: any) => connector.authScheme === params.authScheme + )?.length! > 0 + : appInfo.testConnectors?.length! > 0; + if (hasRelevantTestConnectors) { + integration = await this.integrations.create({ + appId: appInfo.appId, + name: `integration_${timestamp}`, + authScheme: schema, + authConfig: {}, + useComposioAuth: true, + }); - if(!schema) { - throw new Error( - `No supported auth scheme found for \`${String(app)}\`, ` + - "Please create an integration and use the ID to " + - "get the expected parameters." - ); - } + return { + expectedInputFields: integration?.expectedInputFields!, + integrationId: integration?.id!, + authScheme: integration?.authScheme as + | "OAUTH2" + | "OAUTH1" + | "API_KEY" + | "BASIC" + | "BEARER_TOKEN" + | "BASIC_WITH_JWT", + }; + } - integration = await this.integrations.create({ - appId: appInfo.appId, - name: `integration_${timestamp}`, - authScheme: schema, - authConfig: {}, - useComposioAuth: false, - }); + if (!schema) { + throw new Error( + `No supported auth scheme found for \`${String(app)}\`, ` + + "Please create an integration and use the ID to " + + "get the expected parameters." + ); + } - if(!integration) { - throw new Error("An unexpected error occurred while creating the integration, please create an integration manually and use its ID to get the expected parameters"); - } - return { - expectedInputFields: integration.expectedInputFields, - integrationId: integration.id!, - authScheme: integration.authScheme as "OAUTH2" | "OAUTH1" | "API_KEY" | "BASIC" | "BEARER_TOKEN" | "BASIC_WITH_JWT" - } + integration = await this.integrations.create({ + appId: appInfo.appId, + name: `integration_${timestamp}`, + authScheme: schema, + authConfig: {}, + useComposioAuth: false, + }); + + if (!integration) { + throw new Error( + "An unexpected error occurred while creating the integration, please create an integration manually and use its ID to get the expected parameters" + ); } -} \ No newline at end of file + return { + expectedInputFields: integration.expectedInputFields, + integrationId: integration.id!, + authScheme: integration.authScheme as + | "OAUTH2" + | "OAUTH1" + | "API_KEY" + | "BASIC" + | "BEARER_TOKEN" + | "BASIC_WITH_JWT", + }; + } +} diff --git a/js/src/sdk/models/Entity.spec.ts b/js/src/sdk/models/Entity.spec.ts index e00a69931bb..051bd5449cd 100644 --- a/js/src/sdk/models/Entity.spec.ts +++ b/js/src/sdk/models/Entity.spec.ts @@ -25,7 +25,7 @@ describe("Entity class tests", () => { it("should create for different entities", async () => { const entityId = "test-entity"; const entity2 = new Entity(backendClient, entityId); - const connection = await entity2.initiateConnection({appName: "github"}); + const connection = await entity2.initiateConnection({ appName: "github" }); expect(connection.connectionStatus).toBe("INITIATED"); const connection2 = await connectedAccounts.get({ @@ -37,19 +37,23 @@ describe("Entity class tests", () => { it("get connection for github", async () => { const app = "github"; - const connection = await entity.getConnection({app}); + const connection = await entity.getConnection({ app }); expect(connection.appUniqueId).toBe(app); }); it("execute action", async () => { - const connectedAccount = await entity.getConnection({app: "github"}); + const connectedAccount = await entity.getConnection({ app: "github" }); expect(connectedAccount).toHaveProperty("id"); expect(connectedAccount).toHaveProperty("appUniqueId", "github"); const actionName = "GITHUB_GITHUB_API_ROOT".toLowerCase(); const requestBody = {}; - const executionResult = await entity.execute({actionName, params: requestBody, connectedAccountId: connectedAccount.id}); + const executionResult = await entity.execute({ + actionName, + params: requestBody, + connectedAccountId: connectedAccount.id, + }); expect(executionResult).toBeDefined(); expect(executionResult).toHaveProperty("successfull", true); expect(executionResult).toHaveProperty("data.authorizations_url"); @@ -57,7 +61,9 @@ describe("Entity class tests", () => { it("should have an Id of a connected account with label - primary", async () => { const entityW2Connection = new Entity(backendClient, "ckemvy"); - const getConnection = await entityW2Connection.getConnection({app: "github"}); + const getConnection = await entityW2Connection.getConnection({ + app: "github", + }); expect(getConnection).toHaveProperty("id"); }); @@ -89,7 +95,7 @@ describe("Entity class tests", () => { }); it("initiate connection", async () => { - const connection = await entity.initiateConnection({appName: "github"}); + const connection = await entity.initiateConnection({ appName: "github" }); expect(connection.connectionStatus).toBe("INITIATED"); }); }); diff --git a/js/src/sdk/models/Entity.ts b/js/src/sdk/models/Entity.ts index e4766a584d1..f79e1aa9b11 100644 --- a/js/src/sdk/models/Entity.ts +++ b/js/src/sdk/models/Entity.ts @@ -30,10 +30,12 @@ const ZInitiateConnectionParams = z.object({ integrationId: z.string().optional(), authMode: z.string().optional(), connectionData: z.record(z.any()).optional(), - config:z.object({ - labels: z.array(z.string()).optional(), - redirectUrl: z.string().optional(), - }).optional(), + config: z + .object({ + labels: z.array(z.string()).optional(), + redirectUrl: z.string().optional(), + }) + .optional(), }); type TInitiateConnectionParams = z.infer; @@ -59,9 +61,19 @@ export class Entity { this.activeTriggers = new ActiveTriggers(this.backendClient); } - async execute({actionName, params, text, connectedAccountId}: TExecuteActionParams) { + async execute({ + actionName, + params, + text, + connectedAccountId, + }: TExecuteActionParams) { try { - ZExecuteActionParams.parse({actionName, params, text, connectedAccountId}); + ZExecuteActionParams.parse({ + actionName, + params, + text, + connectedAccountId, + }); const action = await this.actionsModel.get({ actionName: actionName, }); @@ -80,13 +92,19 @@ export class Entity { }, }); } - const connectedAccount = await this.getConnection({app: action.appKey, connectedAccountId}); - + const connectedAccount = await this.getConnection({ + app: action.appKey, + connectedAccountId, + }); + if (!connectedAccount) { - throw CEG.getCustomError(SDK_ERROR_CODES.SDK.NO_CONNECTED_ACCOUNT_FOUND, { - message: `Could not find a connection with app='${action.appKey}' and entity='${this.id}'`, - description: `Could not find a connection with app='${action.appKey}' and entity='${this.id}'`, - }); + throw CEG.getCustomError( + SDK_ERROR_CODES.SDK.NO_CONNECTED_ACCOUNT_FOUND, + { + message: `Could not find a connection with app='${action.appKey}' and entity='${this.id}'`, + description: `Could not find a connection with app='${action.appKey}' and entity='${this.id}'`, + } + ); } return this.actionsModel.execute({ actionName: actionName, @@ -103,7 +121,13 @@ export class Entity { } } - async getConnection({app, connectedAccountId}: {app?: string, connectedAccountId?: string}): Promise { + async getConnection({ + app, + connectedAccountId, + }: { + app?: string; + connectedAccountId?: string; + }): Promise { try { if (connectedAccountId) { return await this.connectedAccounts.get({ @@ -122,7 +146,6 @@ export class Entity { return null; } - for (const account of connectedAccounts.items!) { if (account?.labels && account?.labels.includes(LABELS.PRIMARY)) { latestAccount = account; @@ -165,20 +188,27 @@ export class Entity { config: { [key: string]: any } ) { try { - const connectedAccount = await this.getConnection({app}); + const connectedAccount = await this.getConnection({ app }); if (!connectedAccount) { - throw CEG.getCustomError(SDK_ERROR_CODES.SDK.NO_CONNECTED_ACCOUNT_FOUND, { - description: `Could not find a connection with app='${app}' and entity='${this.id}'`, - }); + throw CEG.getCustomError( + SDK_ERROR_CODES.SDK.NO_CONNECTED_ACCOUNT_FOUND, + { + description: `Could not find a connection with app='${app}' and entity='${this.id}'`, + } + ); } - const trigger = await this.triggerModel.setup({connectedAccountId: connectedAccount.id!, triggerName, config}); + const trigger = await this.triggerModel.setup({ + connectedAccountId: connectedAccount.id!, + triggerName, + config, + }); return trigger; } catch (error) { throw CEG.handleAllError(error); } } - async disableTrigger(triggerId: string){ + async disableTrigger(triggerId: string) { try { await this.activeTriggers.disable({ triggerId: triggerId }); return { status: "success" }; @@ -220,10 +250,13 @@ export class Entity { } } - async initiateConnection(data: TInitiateConnectionParams): Promise { + async initiateConnection( + data: TInitiateConnectionParams + ): Promise { try { - const {appName, authMode, authConfig, integrationId, connectionData} = ZInitiateConnectionParams.parse(data); - const {redirectUrl, labels} = data.config || {}; + const { appName, authMode, authConfig, integrationId, connectionData } = + ZInitiateConnectionParams.parse(data); + const { redirectUrl, labels } = data.config || {}; // Get the app details from the client const app = await this.apps.get({ appKey: appName }); diff --git a/js/src/sdk/models/actions.spec.ts b/js/src/sdk/models/actions.spec.ts index c65025507cb..802ba2307aa 100644 --- a/js/src/sdk/models/actions.spec.ts +++ b/js/src/sdk/models/actions.spec.ts @@ -5,73 +5,76 @@ import { Entity } from "./Entity"; import { ConnectedAccounts } from "./connectedAccounts"; describe("Apps class tests", () => { - let backendClient; - let actions: Actions; - let connectedAccouns: ConnectedAccounts + let backendClient; + let actions: Actions; + let connectedAccouns: ConnectedAccounts; - beforeAll(() => { - backendClient = getBackendClient(); - actions = new Actions(backendClient); - connectedAccouns = new ConnectedAccounts(backendClient); - }); + beforeAll(() => { + backendClient = getBackendClient(); + actions = new Actions(backendClient); + connectedAccouns = new ConnectedAccounts(backendClient); + }); - it("should get a specific action", async () => { - const actionName = "GITHUB_GITHUB_API_ROOT"; - const action = await actions.get({ actionName: actionName.toLowerCase() }); - expect(action).toHaveProperty('name', actionName); - }); + it("should get a specific action", async () => { + const actionName = "GITHUB_GITHUB_API_ROOT"; + const action = await actions.get({ actionName: actionName.toLowerCase() }); + expect(action).toHaveProperty("name", actionName); + }); - it("should get a list of actions", async () => { - const actionsList = await actions.list(); - expect(actionsList.items).toBeInstanceOf(Array); - expect(actionsList.items).not.toHaveLength(0); - }); + it("should get a list of actions", async () => { + const actionsList = await actions.list(); + expect(actionsList.items).toBeInstanceOf(Array); + expect(actionsList.items).not.toHaveLength(0); + }); - it("should get a list of actions from integrated apps in an account", async () => { - const actionsList = await actions.list({ - filterByAvailableApps: true - }); - expect(actionsList.items).toBeInstanceOf(Array); - expect(actionsList.items).not.toHaveLength(0); + it("should get a list of actions from integrated apps in an account", async () => { + const actionsList = await actions.list({ + filterByAvailableApps: true, }); + expect(actionsList.items).toBeInstanceOf(Array); + expect(actionsList.items).not.toHaveLength(0); + }); - - it("should execute an action with a connected account for GitHub", async () => { - - const actionName = "GITHUB_GITHUB_API_ROOT".toLowerCase(); - const connectedAccountsResult = await connectedAccouns.list({ appNames: 'github', status: 'ACTIVE' }); - expect(connectedAccountsResult.items).not.toHaveLength(0); - const connectionId = connectedAccountsResult.items[0].id; - - const executionResult = await actions.execute({ - actionName: actionName, - requestBody: { - connectedAccountId: connectionId, - input: {}, - appName: 'github' - } - }); - - expect(executionResult).toHaveProperty('successfull', true); - expect((executionResult as any).data).toHaveProperty('authorizations_url'); + it("should execute an action with a connected account for GitHub", async () => { + const actionName = "GITHUB_GITHUB_API_ROOT".toLowerCase(); + const connectedAccountsResult = await connectedAccouns.list({ + appNames: "github", + status: "ACTIVE", }); + expect(connectedAccountsResult.items).not.toHaveLength(0); + const connectionId = connectedAccountsResult.items[0].id; + const executionResult = await actions.execute({ + actionName: actionName, + requestBody: { + connectedAccountId: connectionId, + input: {}, + appName: "github", + }, + }); - it("should execute an action of noauth app", async () => { - const actionName = "codeinterpreter_execute_code"; - const input = { code_to_execute: 'print("Hello World");' }; + expect(executionResult).toHaveProperty("successfull", true); + expect((executionResult as any).data).toHaveProperty("authorizations_url"); + }); - const executionResult = await actions.execute({ - actionName, - requestBody: { - input: input, - appName: "codeinterpreter", - } - }); + it("should execute an action of noauth app", async () => { + const actionName = "codeinterpreter_execute_code"; + const input = { code_to_execute: 'print("Hello World");' }; - expect(executionResult).toHaveProperty('successfull', true); - //@ts-ignore - expect((executionResult as any).data).toHaveProperty('stdout', 'Hello World\n'); - expect((executionResult as any).data).toHaveProperty('stderr', ''); + const executionResult = await actions.execute({ + actionName, + requestBody: { + input: input, + appName: "codeinterpreter", + }, }); -}); \ No newline at end of file + + expect(executionResult).toHaveProperty("successfull", true); + //@ts-ignore + expect((executionResult as any).data).toHaveProperty( + "stdout", + "Hello World\n" + ); + expect((executionResult as any).data).toHaveProperty("stderr", ""); + }); +}); diff --git a/js/src/sdk/models/actions.ts b/js/src/sdk/models/actions.ts index a5729b5f7a0..fd07fd93f8f 100644 --- a/js/src/sdk/models/actions.ts +++ b/js/src/sdk/models/actions.ts @@ -1,4 +1,8 @@ -import { ActionExecutionReqDTO, ActionProxyRequestConfigDTO, ActionsListResponseDTO } from "../client"; +import { + ActionExecutionReqDTO, + ActionProxyRequestConfigDTO, + ActionsListResponseDTO, +} from "../client"; import apiClient from "../client/client"; import { CEG } from "../utils/error"; import { BackendClient } from "./backendClient"; @@ -22,260 +26,261 @@ import { BackendClient } from "./backendClient"; */ export type GetListActionsData = { - /** - * Name of the apps like "github", "linear" separated by a comma - */ - apps?: string; - /** - * Filter by Action names - */ - actions?: string; - /** - * Filter by Action tags - */ - tags?: string; - /** - * Filter by use case - */ - useCase?: string | undefined; - /** - * Limit of use-cases based search - */ - usecaseLimit?: number; - /** - * Show all actions - i.e disable pagination - */ - showAll?: boolean; - /** - * Show actions enabled for the API Key - */ - showEnabledOnly?: boolean; - /** - * Use smart tag filtering - */ - filterImportantActions?: boolean; - /** - * Should search in available apps only - */ - filterByAvailableApps?: boolean; -} + /** + * Name of the apps like "github", "linear" separated by a comma + */ + apps?: string; + /** + * Filter by Action names + */ + actions?: string; + /** + * Filter by Action tags + */ + tags?: string; + /** + * Filter by use case + */ + useCase?: string | undefined; + /** + * Limit of use-cases based search + */ + usecaseLimit?: number; + /** + * Show all actions - i.e disable pagination + */ + showAll?: boolean; + /** + * Show actions enabled for the API Key + */ + showEnabledOnly?: boolean; + /** + * Use smart tag filtering + */ + filterImportantActions?: boolean; + /** + * Should search in available apps only + */ + filterByAvailableApps?: boolean; +}; export type Parameter = { - /** - * The name of the parameter. - */ - name: string; + /** + * The name of the parameter. + */ + name: string; - /** - * The location of the parameter (e.g., query, header). - */ - in: string; + /** + * The location of the parameter (e.g., query, header). + */ + in: string; - /** - * The value of the parameter. - */ - value: string | number; + /** + * The value of the parameter. + */ + value: string | number; }; export type CustomAuthData = { + /** + * The base URL for the custom authentication. + */ + base_url?: string; + + /** + * An array of parameters for the custom authentication. + */ + parameters: Parameter[]; + + /** + * An optional object containing the body for the custom authentication. + */ + body?: Record; +}; + +export type ExecuteActionData = { + /** + * The name of the action to execute. + */ + actionName: string; + requestBody?: { /** - * The base URL for the custom authentication. + * The unique identifier of the connection to use for executing the action. */ - base_url?: string; - + connectedAccountId?: string; /** - * An array of parameters for the custom authentication. + * An object containing the input parameters for the action. If you want to execute + * NLP based action (i.e text), you can use text parameter instead of input. */ - parameters: Parameter[]; - + input?: { + [key: string]: unknown; + }; + appName?: string; /** - * An optional object containing the body for the custom authentication. + * The text to supply to the action which will be automatically converted to + * appropriate input parameters. */ - body?: Record; -} + text?: string; -export type ExecuteActionData = { /** - * The name of the action to execute. + * The custom authentication configuration for executing the action. */ - actionName: string; - requestBody?: { - /** - * The unique identifier of the connection to use for executing the action. - */ - connectedAccountId?: string; - /** - * An object containing the input parameters for the action. If you want to execute - * NLP based action (i.e text), you can use text parameter instead of input. - */ - input?: { - [key: string]: unknown; - }; - appName?: string; - /** - * The text to supply to the action which will be automatically converted to - * appropriate input parameters. - */ - text?: string; - - /** - * The custom authentication configuration for executing the action. - */ - authConfig?: CustomAuthData; - }; + authConfig?: CustomAuthData; + }; }; export type ExecuteActionResponse = { + /** + * An object containing the details of the action execution. + */ + execution_details?: { /** - * An object containing the details of the action execution. - */ - execution_details?: { - /** - * A boolean indicating whether the action was executed successfully. - * - */ - executed?: boolean; - }; - /** - * An object containing the response data from the action execution. + * A boolean indicating whether the action was executed successfully. + * */ - response_data?: { - [key: string]: unknown; - }; + executed?: boolean; + }; + /** + * An object containing the response data from the action execution. + */ + response_data?: { + [key: string]: unknown; + }; }; export class Actions { - backendClient: BackendClient; + backendClient: BackendClient; - constructor(backendClient: BackendClient) { - this.backendClient = backendClient; - } + constructor(backendClient: BackendClient) { + this.backendClient = backendClient; + } - /** - * Retrieves details of a specific action in the Composio platform by providing its action name. - * - * The response includes the action's name, display name, description, input parameters, expected response, associated app information, and enabled status. - * - * @param {GetActionData} data The data for the request. - * @returns {CancelablePromise} A promise that resolves to the details of the action. - * @throws {ApiError} If the request fails. - */ - async get(data: { actionName: string; }) { - try{ - const actions = await apiClient.actionsV2.getActionV2({ - path: { - actionId: data.actionName - } - }); + /** + * Retrieves details of a specific action in the Composio platform by providing its action name. + * + * The response includes the action's name, display name, description, input parameters, expected response, associated app information, and enabled status. + * + * @param {GetActionData} data The data for the request. + * @returns {CancelablePromise} A promise that resolves to the details of the action. + * @throws {ApiError} If the request fails. + */ + async get(data: { actionName: string }) { + try { + const actions = await apiClient.actionsV2.getActionV2({ + path: { + actionId: data.actionName, + }, + }); - return (actions.data!); - } catch(e){ - throw CEG.handleAllError(e) - } + return actions.data!; + } catch (e) { + throw CEG.handleAllError(e); } + } - /** - * Retrieves a list of all actions in the Composio platform. - * - * This method allows you to fetch a list of all the available actions. It supports pagination to handle large numbers of actions. The response includes an array of action objects, each containing information such as the action's name, display name, description, input parameters, expected response, associated app information, and enabled status. - * - * @param {GetListActionsData} data The data for the request. - * @returns {Promise} A promise that resolves to the list of all actions. - * @throws {ApiError} If the request fails. - */ - async list(data: GetListActionsData = {}): Promise { - try { + /** + * Retrieves a list of all actions in the Composio platform. + * + * This method allows you to fetch a list of all the available actions. It supports pagination to handle large numbers of actions. The response includes an array of action objects, each containing information such as the action's name, display name, description, input parameters, expected response, associated app information, and enabled status. + * + * @param {GetListActionsData} data The data for the request. + * @returns {Promise} A promise that resolves to the list of all actions. + * @throws {ApiError} If the request fails. + */ + async list(data: GetListActionsData = {}): Promise { + try { + let apps = data.apps; - let apps = data.apps; - - // Throw error if user has provided both filterByAvailableApps and apps - if(data?.filterByAvailableApps && data?.apps){ - throw new Error("Both filterByAvailableApps and apps cannot be provided together"); - } + // Throw error if user has provided both filterByAvailableApps and apps + if (data?.filterByAvailableApps && data?.apps) { + throw new Error( + "Both filterByAvailableApps and apps cannot be provided together" + ); + } - if(data?.filterByAvailableApps){ - // Todo: To create a new API to get all integrated apps for a user instead of fetching all apps - const integratedApps = await apiClient.appConnector.listAllConnectors(); - apps = integratedApps.data?.items.map((app)=> app?.appName).join(","); - } - - const response = await apiClient.actionsV2.listActionsV2({ - query: { - actions: data.actions, - apps: apps, - showAll: data.showAll, - tags: data.tags, - useCase: data.useCase as string, - filterImportantActions: data.filterImportantActions, - showEnabledOnly: data.showEnabledOnly, - usecaseLimit: data.usecaseLimit || undefined - } - }); - return response.data!; - } catch (error) { - throw CEG.handleAllError(error); - } + if (data?.filterByAvailableApps) { + // Todo: To create a new API to get all integrated apps for a user instead of fetching all apps + const integratedApps = await apiClient.appConnector.listAllConnectors(); + apps = integratedApps.data?.items.map((app) => app?.appName).join(","); + } + + const response = await apiClient.actionsV2.listActionsV2({ + query: { + actions: data.actions, + apps: apps, + showAll: data.showAll, + tags: data.tags, + useCase: data.useCase as string, + filterImportantActions: data.filterImportantActions, + showEnabledOnly: data.showEnabledOnly, + usecaseLimit: data.usecaseLimit || undefined, + }, + }); + return response.data!; + } catch (error) { + throw CEG.handleAllError(error); } + } - /** - * Executes a specific action in the Composio platform. - * - * This method allows you to trigger the execution of an action by providing its name and the necessary input parameters. The request includes the connected account ID to identify the app connection to use for the action, and the input parameters required by the action. The response provides details about the execution status and the response data returned by the action. - * - * @param {ExecuteActionData} data The data for the request. - * @returns {Promise} A promise that resolves to the execution status and response data. - * @throws {ApiError} If the request fails. - */ - async execute(data: ExecuteActionData){ - try { - const { data: res } = await apiClient.actionsV2.executeActionV2({ - body: data.requestBody as unknown as ActionExecutionReqDTO, - path: { - actionId: data.actionName - } - }); - return res!; - } catch (error) { - throw CEG.handleAllError(error); - } + /** + * Executes a specific action in the Composio platform. + * + * This method allows you to trigger the execution of an action by providing its name and the necessary input parameters. The request includes the connected account ID to identify the app connection to use for the action, and the input parameters required by the action. The response provides details about the execution status and the response data returned by the action. + * + * @param {ExecuteActionData} data The data for the request. + * @returns {Promise} A promise that resolves to the execution status and response data. + * @throws {ApiError} If the request fails. + */ + async execute(data: ExecuteActionData) { + try { + const { data: res } = await apiClient.actionsV2.executeActionV2({ + body: data.requestBody as unknown as ActionExecutionReqDTO, + path: { + actionId: data.actionName, + }, + }); + return res!; + } catch (error) { + throw CEG.handleAllError(error); } + } - async findActionEnumsByUseCase(data: { - apps: Array, - useCase: string, - limit?: number, - }): Promise> { - try { - const { data: res } = await apiClient.actionsV2.advancedUseCaseSearch({ - query: { - apps: data.apps.join(","), - useCase: data.useCase, - limit: data.limit || undefined - } - }); - return res!.items.map((item) => item.actions).flat() || []; - } catch (error) { - throw CEG.handleAllError(error); - } + async findActionEnumsByUseCase(data: { + apps: Array; + useCase: string; + limit?: number; + }): Promise> { + try { + const { data: res } = await apiClient.actionsV2.advancedUseCaseSearch({ + query: { + apps: data.apps.join(","), + useCase: data.useCase, + limit: data.limit || undefined, + }, + }); + return res!.items.map((item) => item.actions).flat() || []; + } catch (error) { + throw CEG.handleAllError(error); } + } - /** - * Executes a action using Composio Proxy - * - * This method allows you to trigger the execution of an action by providing its name and the necessary input parameters. The request includes the connected account ID to identify the app connection to use for the action, and the input parameters required by the action. The response provides details about the execution status and the response data returned by the action. - * - * @param {ExecuteActionData} data The data for the request. - * @returns {Promise} A promise that resolves to the execution status and response data. - * @throws {ApiError} If the request fails. - */ + /** + * Executes a action using Composio Proxy + * + * This method allows you to trigger the execution of an action by providing its name and the necessary input parameters. The request includes the connected account ID to identify the app connection to use for the action, and the input parameters required by the action. The response provides details about the execution status and the response data returned by the action. + * + * @param {ExecuteActionData} data The data for the request. + * @returns {Promise} A promise that resolves to the execution status and response data. + * @throws {ApiError} If the request fails. + */ - async executeRequest(data: ActionProxyRequestConfigDTO){ - try { - const { data: res } = await apiClient.actionsV2.executeActionProxyV2({ - body: data as unknown as ActionProxyRequestConfigDTO - }); - return res!; - } catch (error) { - throw CEG.handleAllError(error); - } + async executeRequest(data: ActionProxyRequestConfigDTO) { + try { + const { data: res } = await apiClient.actionsV2.executeActionProxyV2({ + body: data as unknown as ActionProxyRequestConfigDTO, + }); + return res!; + } catch (error) { + throw CEG.handleAllError(error); } -} \ No newline at end of file + } +} diff --git a/js/src/sdk/models/activeTriggers.spec.ts b/js/src/sdk/models/activeTriggers.spec.ts index 004152bf7eb..4ba70984149 100644 --- a/js/src/sdk/models/activeTriggers.spec.ts +++ b/js/src/sdk/models/activeTriggers.spec.ts @@ -3,31 +3,30 @@ import { getBackendClient } from "../testUtils/getBackendClient"; import { ActiveTriggers } from "./activeTriggers"; describe("Active Triggers class tests", () => { - let backendClient; - let activeTriggers: ActiveTriggers; - - beforeAll(() => { - backendClient = getBackendClient(); - activeTriggers = new ActiveTriggers(backendClient); - }); + let backendClient; + let activeTriggers: ActiveTriggers; - it("should retrieve a list of active triggers", async () => { - const activeTriggersList = await activeTriggers.list(); - expect(activeTriggersList).toBeInstanceOf(Array); - expect(activeTriggersList).not.toHaveLength(0); - }); + beforeAll(() => { + backendClient = getBackendClient(); + activeTriggers = new ActiveTriggers(backendClient); + }); - it("should retrieve details of a specific active trigger", async () => { - const activeTriggersList = await activeTriggers.list(); - const firstTrigger = activeTriggersList[0]; + it("should retrieve a list of active triggers", async () => { + const activeTriggersList = await activeTriggers.list(); + expect(activeTriggersList).toBeInstanceOf(Array); + expect(activeTriggersList).not.toHaveLength(0); + }); - if (!firstTrigger.id) { - throw new Error("Trigger ID is required"); - } - const activeTrigger = await activeTriggers.get({ - triggerId: firstTrigger.id as string - }); - expect(activeTrigger).toBeDefined(); - }); + it("should retrieve details of a specific active trigger", async () => { + const activeTriggersList = await activeTriggers.list(); + const firstTrigger = activeTriggersList[0]; -}); \ No newline at end of file + if (!firstTrigger.id) { + throw new Error("Trigger ID is required"); + } + const activeTrigger = await activeTriggers.get({ + triggerId: firstTrigger.id as string, + }); + expect(activeTrigger).toBeDefined(); + }); +}); diff --git a/js/src/sdk/models/activeTriggers.ts b/js/src/sdk/models/activeTriggers.ts index 1915a2d516b..cde2c76a476 100644 --- a/js/src/sdk/models/activeTriggers.ts +++ b/js/src/sdk/models/activeTriggers.ts @@ -1,103 +1,103 @@ - import { GetActiveTriggersData } from "../client/types.gen"; -import apiClient from "../client/client" +import apiClient from "../client/client"; import { BackendClient } from "./backendClient"; import { CEG } from "../utils/error"; type TActiveTrigger = { - id: string; - connectionId: string; - triggerName: string; - triggerData: string; - triggerConfig: Record; - state: Record; - createdAt: string; - updatedAt: string; - disabledAt: string | null; - disabledReason: string | null; -} + id: string; + connectionId: string; + triggerName: string; + triggerData: string; + triggerConfig: Record; + state: Record; + createdAt: string; + updatedAt: string; + disabledAt: string | null; + disabledReason: string | null; +}; export class ActiveTriggers { + backendClient: BackendClient; - backendClient: BackendClient; - - constructor(backendClient: BackendClient) { - this.backendClient = backendClient; - } - /** - * Retrieves details of a specific active trigger in the Composio platform by providing its trigger name. - * - * The response includes the trigger's name, description, input parameters, expected response, associated app information, and enabled status. - * - * @param {GetActiveTriggerData} data The data for the request. - * @returns {CancelablePromise} A promise that resolves to the details of the active trigger. - * @throws {ApiError} If the request fails. - */ - async get({triggerId}: {triggerId: string}) { - try { - const {data} = await apiClient.triggers.getActiveTriggers({ - query:{ - triggerIds : `${triggerId}` - } - }) - return data?.triggers[0]; - } catch (error) { - throw CEG.handleAllError(error); - } + constructor(backendClient: BackendClient) { + this.backendClient = backendClient; + } + /** + * Retrieves details of a specific active trigger in the Composio platform by providing its trigger name. + * + * The response includes the trigger's name, description, input parameters, expected response, associated app information, and enabled status. + * + * @param {GetActiveTriggerData} data The data for the request. + * @returns {CancelablePromise} A promise that resolves to the details of the active trigger. + * @throws {ApiError} If the request fails. + */ + async get({ triggerId }: { triggerId: string }) { + try { + const { data } = await apiClient.triggers.getActiveTriggers({ + query: { + triggerIds: `${triggerId}`, + }, + }); + return data?.triggers[0]; + } catch (error) { + throw CEG.handleAllError(error); } + } + + /** + * Retrieves a list of all active triggers in the Composio platform. + * + * This method allows you to fetch a list of all the available active triggers. It supports pagination to handle large numbers of triggers. The response includes an array of trigger objects, each containing information such as the trigger's name, description, input parameters, expected response, associated app information, and enabled status. + * + * @param {ListActiveTriggersData} data The data for the request. + * @returns {CancelablePromise} A promise that resolves to the list of all active triggers. + * @throws {ApiError} If the request fails. + */ + async list(data: GetActiveTriggersData = {}) { + try { + const { data: response } = await apiClient.triggers.getActiveTriggers({ + query: data, + }); - /** - * Retrieves a list of all active triggers in the Composio platform. - * - * This method allows you to fetch a list of all the available active triggers. It supports pagination to handle large numbers of triggers. The response includes an array of trigger objects, each containing information such as the trigger's name, description, input parameters, expected response, associated app information, and enabled status. - * - * @param {ListActiveTriggersData} data The data for the request. - * @returns {CancelablePromise} A promise that resolves to the list of all active triggers. - * @throws {ApiError} If the request fails. - */ - async list(data: GetActiveTriggersData = {}) { - try { - const {data: response} = await apiClient.triggers.getActiveTriggers({ query: data }) - - return response?.triggers || []; - } catch (error) { - throw CEG.handleAllError(error); - } + return response?.triggers || []; + } catch (error) { + throw CEG.handleAllError(error); } + } - /** - * Enables the previously disabled trigger. - * - * @param {Object} data The data for the request. - * @param {string} data.triggerId Id of the trigger - * @returns {CancelablePromise>} A promise that resolves to the response of the enable request. - * @throws {ApiError} If the request fails. - */ - async enable(data: { triggerId: string }): Promise { - try { - await apiClient.triggers.switchTriggerInstanceStatus({ - path: data, - body: { - enabled: true - } - }); - return true; - } catch (error) { - throw CEG.handleAllError(error); - } + /** + * Enables the previously disabled trigger. + * + * @param {Object} data The data for the request. + * @param {string} data.triggerId Id of the trigger + * @returns {CancelablePromise>} A promise that resolves to the response of the enable request. + * @throws {ApiError} If the request fails. + */ + async enable(data: { triggerId: string }): Promise { + try { + await apiClient.triggers.switchTriggerInstanceStatus({ + path: data, + body: { + enabled: true, + }, + }); + return true; + } catch (error) { + throw CEG.handleAllError(error); } + } - async disable(data: { triggerId: string }) { - try { - await apiClient.triggers.switchTriggerInstanceStatus({ - path: data, - body: { - enabled: false - } - }); - return true; - } catch (error) { - throw CEG.handleAllError(error); - } + async disable(data: { triggerId: string }) { + try { + await apiClient.triggers.switchTriggerInstanceStatus({ + path: data, + body: { + enabled: false, + }, + }); + return true; + } catch (error) { + throw CEG.handleAllError(error); } -} \ No newline at end of file + } +} diff --git a/js/src/sdk/models/apps.spec.ts b/js/src/sdk/models/apps.spec.ts index 247ccfa12ab..388db34b763 100644 --- a/js/src/sdk/models/apps.spec.ts +++ b/js/src/sdk/models/apps.spec.ts @@ -3,66 +3,77 @@ import { getBackendClient } from "../testUtils/getBackendClient"; import { Apps } from "./apps"; describe("Apps class tests", () => { - let backendClient; - let apps: Apps; + let backendClient; + let apps: Apps; - beforeAll(() => { - backendClient = getBackendClient(); - apps = new Apps(backendClient); - }); + beforeAll(() => { + backendClient = getBackendClient(); + apps = new Apps(backendClient); + }); - it("should create an Apps instance and retrieve apps list", async () => { - const appsList = await apps.list(); - expect(appsList).toBeInstanceOf(Array); - expect(appsList).not.toHaveLength(0); + it("should create an Apps instance and retrieve apps list", async () => { + const appsList = await apps.list(); + expect(appsList).toBeInstanceOf(Array); + expect(appsList).not.toHaveLength(0); - const firstItem = appsList[0]; - expect(firstItem).toHaveProperty('appId'); - expect(firstItem).toHaveProperty('key'); - expect(firstItem).toHaveProperty('name'); - }); + const firstItem = appsList[0]; + expect(firstItem).toHaveProperty("appId"); + expect(firstItem).toHaveProperty("key"); + expect(firstItem).toHaveProperty("name"); + }); - it("should get details of a specific app by key", async () => { - const appKey = "github"; - const app = await apps.get({ appKey }); - expect(app).toBeDefined(); - expect(app).toHaveProperty('auth_schemes'); - // @ts-ignore - expect(app.auth_schemes[0]).toHaveProperty('auth_mode', 'OAUTH2'); - expect(app).toHaveProperty('key', appKey); - expect(app).toHaveProperty('name', 'github'); - expect(app).toHaveProperty('description'); - }); + it("should get details of a specific app by key", async () => { + const appKey = "github"; + const app = await apps.get({ appKey }); + expect(app).toBeDefined(); + expect(app).toHaveProperty("auth_schemes"); + // @ts-ignore + expect(app.auth_schemes[0]).toHaveProperty("auth_mode", "OAUTH2"); + expect(app).toHaveProperty("key", appKey); + expect(app).toHaveProperty("name", "github"); + expect(app).toHaveProperty("description"); + }); - it("should return undefined for an invalid app key", async () => { - try { - const app = await apps.get({ appKey: "nonexistent_key" }); - expect(app).toBeUndefined(); - } catch (error) { - expect(error).toBeDefined(); - } - }); + it("should return undefined for an invalid app key", async () => { + try { + const app = await apps.get({ appKey: "nonexistent_key" }); + expect(app).toBeUndefined(); + } catch (error) { + expect(error).toBeDefined(); + } + }); - it("should get required params for a specific app", async () => { - const inputFields = await apps.getRequiredParams("shopify"); + it("should get required params for a specific app", async () => { + const inputFields = await apps.getRequiredParams("shopify"); - expect(inputFields).toHaveProperty('availableAuthSchemes'); - expect(inputFields).toHaveProperty('authSchemes'); + expect(inputFields).toHaveProperty("availableAuthSchemes"); + expect(inputFields).toHaveProperty("authSchemes"); - const OAUTH2_SCHEME = "OAUTH2"; - expect(inputFields.availableAuthSchemes).toContain(OAUTH2_SCHEME); - expect(inputFields.authSchemes[OAUTH2_SCHEME].expected_from_user).toEqual(["client_id", "client_secret"]); - expect(inputFields.authSchemes[OAUTH2_SCHEME].optional_fields).toEqual(["oauth_redirect_uri", "scopes"]); - expect(inputFields.authSchemes[OAUTH2_SCHEME].required_fields).toEqual(["shop"]); - }); + const OAUTH2_SCHEME = "OAUTH2"; + expect(inputFields.availableAuthSchemes).toContain(OAUTH2_SCHEME); + expect(inputFields.authSchemes[OAUTH2_SCHEME].expected_from_user).toEqual([ + "client_id", + "client_secret", + ]); + expect(inputFields.authSchemes[OAUTH2_SCHEME].optional_fields).toEqual([ + "oauth_redirect_uri", + "scopes", + ]); + expect(inputFields.authSchemes[OAUTH2_SCHEME].required_fields).toEqual([ + "shop", + ]); + }); - it("should get required params for a specific auth scheme", async () => { - const OAUTH2_SCHEME = "OAUTH2"; - const requiredParams = await apps.getRequiredParamsForAuthScheme({appId: "shopify", authScheme: OAUTH2_SCHEME}); - expect(requiredParams).toEqual({ - required_fields: ["shop"], - optional_fields: ["oauth_redirect_uri", "scopes"], - expected_from_user: ["client_id", "client_secret"] - }); + it("should get required params for a specific auth scheme", async () => { + const OAUTH2_SCHEME = "OAUTH2"; + const requiredParams = await apps.getRequiredParamsForAuthScheme({ + appId: "shopify", + authScheme: OAUTH2_SCHEME, + }); + expect(requiredParams).toEqual({ + required_fields: ["shop"], + optional_fields: ["oauth_redirect_uri", "scopes"], + expected_from_user: ["client_id", "client_secret"], }); -}); \ No newline at end of file + }); +}); diff --git a/js/src/sdk/models/apps.ts b/js/src/sdk/models/apps.ts index 13e5407062f..d95f33a249f 100644 --- a/js/src/sdk/models/apps.ts +++ b/js/src/sdk/models/apps.ts @@ -1,11 +1,11 @@ -import { AppListResDTO, SingleAppInfoResDTO } from "../client"; -import apiClient from "../client/client" +import { AppListResDTO, SingleAppInfoResDTO } from "../client"; +import apiClient from "../client/client"; import { CEG } from "../utils/error"; import { BackendClient } from "./backendClient"; export type GetAppData = { - appKey: string; + appKey: string; }; export type GetAppResponse = SingleAppInfoResDTO; @@ -13,118 +13,126 @@ export type GetAppResponse = SingleAppInfoResDTO; export type ListAllAppsResponse = AppListResDTO; export type RequiredParamsResponse = { - required_fields: string[]; - expected_from_user: string[]; - optional_fields: string[]; + required_fields: string[]; + expected_from_user: string[]; + optional_fields: string[]; }; export type RequiredParamsFullResponse = { - availableAuthSchemes: string[]; - authSchemes: Record; + availableAuthSchemes: string[]; + authSchemes: Record; }; export class Apps { - backendClient: BackendClient; - constructor(backendClient: BackendClient) { - this.backendClient = backendClient; - } - + backendClient: BackendClient; + constructor(backendClient: BackendClient) { + this.backendClient = backendClient; + } - /** - * Retrieves a list of all available apps in the Composio platform. - * - * This method allows clients to explore and discover the supported apps. It returns an array of app objects, each containing essential details such as the app's key, name, description, logo, categories, and unique identifier. - * - * @returns {Promise} A promise that resolves to the list of all apps. - * @throws {ApiError} If the request fails. - */ - async list(){ - try { - const {data} = await apiClient.apps.getApps(); - return data?.items || []; - } catch (error) { - throw CEG.handleAllError(error); - } + /** + * Retrieves a list of all available apps in the Composio platform. + * + * This method allows clients to explore and discover the supported apps. It returns an array of app objects, each containing essential details such as the app's key, name, description, logo, categories, and unique identifier. + * + * @returns {Promise} A promise that resolves to the list of all apps. + * @throws {ApiError} If the request fails. + */ + async list() { + try { + const { data } = await apiClient.apps.getApps(); + return data?.items || []; + } catch (error) { + throw CEG.handleAllError(error); } + } - /** - * Retrieves details of a specific app in the Composio platform. - * - * This method allows clients to fetch detailed information about a specific app by providing its unique key. The response includes the app's name, key, status, description, logo, categories, authentication schemes, and other metadata. - * - * @param {GetAppData} data The data for the request, including the app's unique key. - * @returns {CancelablePromise} A promise that resolves to the details of the app. - * @throws {ApiError} If the request fails. - */ - async get(data: GetAppData){ - try { - const {data:response} = await apiClient.apps.getApp({ - path: { - appName: data.appKey - } - }); - if(!response) throw new Error("App not found"); - return response; - } catch (error) { - throw CEG.handleAllError(error); - } + /** + * Retrieves details of a specific app in the Composio platform. + * + * This method allows clients to fetch detailed information about a specific app by providing its unique key. The response includes the app's name, key, status, description, logo, categories, authentication schemes, and other metadata. + * + * @param {GetAppData} data The data for the request, including the app's unique key. + * @returns {CancelablePromise} A promise that resolves to the details of the app. + * @throws {ApiError} If the request fails. + */ + async get(data: GetAppData) { + try { + const { data: response } = await apiClient.apps.getApp({ + path: { + appName: data.appKey, + }, + }); + if (!response) throw new Error("App not found"); + return response; + } catch (error) { + throw CEG.handleAllError(error); } + } + + async getRequiredParams(appId: string): Promise { + try { + const appData = await this.get({ appKey: appId }); + if (!appData) throw new Error("App not found"); + const authSchemes = appData.auth_schemes; + const availableAuthSchemes = ( + authSchemes as Array<{ mode: string }> + )?.map((scheme) => scheme?.mode); + + const authSchemesObject: Record = {}; + + for (const scheme of authSchemes as Array<{ + mode: string; + fields: Array<{ + name: string; + required: boolean; + expected_from_customer: boolean; + }>; + }>) { + const name = scheme.mode; + authSchemesObject[name] = { + required_fields: [], + optional_fields: [], + expected_from_user: [], + }; + + scheme.fields.forEach((field) => { + const isExpectedForIntegrationSetup = + field.expected_from_customer === false; + const isRequired = field.required; - async getRequiredParams(appId: string): Promise { - try { - const appData = await this.get({ appKey: appId }); - if(!appData) throw new Error("App not found"); - const authSchemes = appData.auth_schemes; - const availableAuthSchemes = (authSchemes as Array<{ mode: string }>)?.map(scheme => scheme?.mode); - - const authSchemesObject: Record = {}; - - for (const scheme of authSchemes as Array<{ - mode: string; - fields: Array<{ - name: string; - required: boolean; - expected_from_customer: boolean; - }>; - }>) { - const name = scheme.mode; - authSchemesObject[name] = { - required_fields: [], - optional_fields: [], - expected_from_user: [] - }; - - scheme.fields.forEach((field) => { - const isExpectedForIntegrationSetup = field.expected_from_customer === false; - const isRequired = field.required; - - if (isExpectedForIntegrationSetup) { - if (isRequired) { - authSchemesObject[name].expected_from_user.push(field.name); - } else { - authSchemesObject[name].optional_fields.push(field.name); - } - } else { - authSchemesObject[name].required_fields.push(field.name); - } - }); + if (isExpectedForIntegrationSetup) { + if (isRequired) { + authSchemesObject[name].expected_from_user.push(field.name); + } else { + authSchemesObject[name].optional_fields.push(field.name); } + } else { + authSchemesObject[name].required_fields.push(field.name); + } + }); + } - return { - availableAuthSchemes, - authSchemes: authSchemesObject - }; - } catch (error) { - throw CEG.handleAllError(error); - } + return { + availableAuthSchemes, + authSchemes: authSchemesObject, + }; + } catch (error) { + throw CEG.handleAllError(error); } + } - async getRequiredParamsForAuthScheme({appId, authScheme}: {appId: string, authScheme: string}): Promise { - try { - const params = await this.getRequiredParams(appId); - return params.authSchemes[authScheme]; - } catch (error) { - throw CEG.handleAllError(error); - } + async getRequiredParamsForAuthScheme({ + appId, + authScheme, + }: { + appId: string; + authScheme: string; + }): Promise { + try { + const params = await this.getRequiredParams(appId); + return params.authSchemes[authScheme]; + } catch (error) { + throw CEG.handleAllError(error); } + } } diff --git a/js/src/sdk/models/backendClient.spec.ts b/js/src/sdk/models/backendClient.spec.ts index a2b2a74db23..39eb1f26374 100644 --- a/js/src/sdk/models/backendClient.spec.ts +++ b/js/src/sdk/models/backendClient.spec.ts @@ -4,23 +4,29 @@ import { BACKEND_CONFIG, getTestConfig } from "../../../config/getTestConfig"; import { BackendClient } from "./backendClient"; describe("Apps class tests", () => { - let backendClient; - let testConfig:BACKEND_CONFIG; + let backendClient; + let testConfig: BACKEND_CONFIG; - beforeAll(() => { - testConfig = getTestConfig(); - }); + beforeAll(() => { + testConfig = getTestConfig(); + }); - it("should create an Apps instance and retrieve apps list", async () => { - backendClient = new BackendClient(testConfig.COMPOSIO_API_KEY, testConfig.BACKEND_HERMES_URL); - }); + it("should create an Apps instance and retrieve apps list", async () => { + backendClient = new BackendClient( + testConfig.COMPOSIO_API_KEY, + testConfig.BACKEND_HERMES_URL + ); + }); - it("should throw an error if api key is not provided", async () => { - expect(() => new BackendClient("", testConfig.BACKEND_HERMES_URL)).toThrow('🔑 API Key Missing or Invalid'); - }); + it("should throw an error if api key is not provided", async () => { + expect(() => new BackendClient("", testConfig.BACKEND_HERMES_URL)).toThrow( + "🔑 API Key Missing or Invalid" + ); + }); - it("should throw and error if wrong base url is provided", async () => { - expect(() => new BackendClient(testConfig.COMPOSIO_API_KEY, "htt://wrong.url")).toThrow('🔗 Base URL htt://wrong.url is not valid'); - }); - -}); \ No newline at end of file + it("should throw and error if wrong base url is provided", async () => { + expect( + () => new BackendClient(testConfig.COMPOSIO_API_KEY, "htt://wrong.url") + ).toThrow("🔗 Base URL htt://wrong.url is not valid"); + }); +}); diff --git a/js/src/sdk/models/backendClient.ts b/js/src/sdk/models/backendClient.ts index e098f61e0e0..d0e635e4cfc 100644 --- a/js/src/sdk/models/backendClient.ts +++ b/js/src/sdk/models/backendClient.ts @@ -1,5 +1,5 @@ -import apiClient from "../client/client" -import { client as axiosClient } from "../client/services.gen" +import apiClient from "../client/client"; +import { client as axiosClient } from "../client/services.gen"; import { setAxiosClientConfig } from "../utils/config"; import { CEG } from "../utils/error"; import { SDK_ERROR_CODES } from "../utils/errors/src/constants"; @@ -8,77 +8,77 @@ import { SDK_ERROR_CODES } from "../utils/errors/src/constants"; * Class representing the details required to initialize and configure the API client. */ export class BackendClient { - /** - * The API key used for authenticating requests. - */ - public apiKey: string; + /** + * The API key used for authenticating requests. + */ + public apiKey: string; - /** - * The base URL of the API against which requests will be made. - */ - public baseUrl: string; + /** + * The base URL of the API against which requests will be made. + */ + public baseUrl: string; - /** - * The runtime environment where the client is being used. - */ - public runtime: string; + /** + * The runtime environment where the client is being used. + */ + public runtime: string; - /** - * Creates an instance of apiClientDetails. - * @param {string} apiKey - The API key for client initialization. - * @param {string} baseUrl - The base URL for the API client. - * @param {string} runtime - The runtime environment identifier. - * @throws Will throw an error if the API key is not provided. - */ - constructor(apiKey: string, baseUrl: string, runtime?: string) { - this.runtime = runtime || ''; - this.apiKey = apiKey; - this.baseUrl = baseUrl; + /** + * Creates an instance of apiClientDetails. + * @param {string} apiKey - The API key for client initialization. + * @param {string} baseUrl - The base URL for the API client. + * @param {string} runtime - The runtime environment identifier. + * @throws Will throw an error if the API key is not provided. + */ + constructor(apiKey: string, baseUrl: string, runtime?: string) { + this.runtime = runtime || ""; + this.apiKey = apiKey; + this.baseUrl = baseUrl; - if (!apiKey) { - throw CEG.getCustomError(SDK_ERROR_CODES.COMMON.API_KEY_UNAVAILABLE,{}); - } - - // Validate baseUrl - if (!baseUrl.startsWith("http://") && !baseUrl.startsWith("https://")) { - throw CEG.getCustomError(SDK_ERROR_CODES.COMMON.BASE_URL_NOT_REACHABLE,{ - message: `🔗 Base URL ${baseUrl} is not valid`, - description: "The composio backend URL provided is not valid" - }); - } - - this.initializeApiClient(); + if (!apiKey) { + throw CEG.getCustomError(SDK_ERROR_CODES.COMMON.API_KEY_UNAVAILABLE, {}); } - /** - * Retrieves the client ID from the user's information. - * @returns {Promise} A promise that resolves to the client ID. - * @throws Will throw an error if the HTTP request fails. - */ - public async getClientId(): Promise { - try { - const {data} = await apiClient.clientAuth.getUserInfo() - return data?.client?.id || ''; - } catch (error) { - throw CEG.handleAllError(error); - } + // Validate baseUrl + if (!baseUrl.startsWith("http://") && !baseUrl.startsWith("https://")) { + throw CEG.getCustomError(SDK_ERROR_CODES.COMMON.BASE_URL_NOT_REACHABLE, { + message: `🔗 Base URL ${baseUrl} is not valid`, + description: "The composio backend URL provided is not valid", + }); } - /** - * Initializes the API client with the provided configuration. - * @private - */ - private initializeApiClient() { - axiosClient.setConfig({ - baseURL: this.baseUrl, - headers: { - 'X-API-KEY': `${this.apiKey}`, - 'X-SOURCE': 'js_sdk', - 'X-RUNTIME': this.runtime - }, - throwOnError: true - }); + this.initializeApiClient(); + } - setAxiosClientConfig(axiosClient.instance); + /** + * Retrieves the client ID from the user's information. + * @returns {Promise} A promise that resolves to the client ID. + * @throws Will throw an error if the HTTP request fails. + */ + public async getClientId(): Promise { + try { + const { data } = await apiClient.clientAuth.getUserInfo(); + return data?.client?.id || ""; + } catch (error) { + throw CEG.handleAllError(error); } -} \ No newline at end of file + } + + /** + * Initializes the API client with the provided configuration. + * @private + */ + private initializeApiClient() { + axiosClient.setConfig({ + baseURL: this.baseUrl, + headers: { + "X-API-KEY": `${this.apiKey}`, + "X-SOURCE": "js_sdk", + "X-RUNTIME": this.runtime, + }, + throwOnError: true, + }); + + setAxiosClientConfig(axiosClient.instance); + } +} diff --git a/js/src/sdk/models/connectedAccounts.spec.ts b/js/src/sdk/models/connectedAccounts.spec.ts index 7cfdde51414..500d74ab4f8 100644 --- a/js/src/sdk/models/connectedAccounts.spec.ts +++ b/js/src/sdk/models/connectedAccounts.spec.ts @@ -3,59 +3,60 @@ import { getBackendClient } from "../testUtils/getBackendClient"; import { ConnectedAccounts } from "./connectedAccounts"; describe("ConnectedAccounts class tests", () => { - let backendClient; - let connectedAccounts: ConnectedAccounts; - - beforeAll(() => { - backendClient = getBackendClient(); - connectedAccounts = new ConnectedAccounts(backendClient); - }); - - it("should create a ConnectedAccounts instance and retrieve connections list", async () => { - // @ts-ignore - const connectionsData: TConnectionData = { - appNames: 'github' - }; - const connectionsList = await connectedAccounts.list(connectionsData); - expect(connectionsList.items).toBeInstanceOf(Array); - expect(connectionsList.items).not.toHaveLength(0); - - // @ts-ignore - const firstConnection = connectionsList.items[0]; - expect(firstConnection.appName).toBe('github'); - expect(firstConnection).toHaveProperty('clientUniqueUserId'); - expect(firstConnection).toHaveProperty('status'); - expect(firstConnection).toHaveProperty('connectionParams'); + let backendClient; + let connectedAccounts: ConnectedAccounts; + + beforeAll(() => { + backendClient = getBackendClient(); + connectedAccounts = new ConnectedAccounts(backendClient); + }); + + it("should create a ConnectedAccounts instance and retrieve connections list", async () => { + // @ts-ignore + const connectionsData: TConnectionData = { + appNames: "github", + }; + const connectionsList = await connectedAccounts.list(connectionsData); + expect(connectionsList.items).toBeInstanceOf(Array); + expect(connectionsList.items).not.toHaveLength(0); + + // @ts-ignore + const firstConnection = connectionsList.items[0]; + expect(firstConnection.appName).toBe("github"); + expect(firstConnection).toHaveProperty("clientUniqueUserId"); + expect(firstConnection).toHaveProperty("status"); + expect(firstConnection).toHaveProperty("connectionParams"); + }); + + it("should retrieve a specific connection", async () => { + // @ts-ignore + const connectionsData: TConnectionData = { + appNames: "github", + }; + const connectionsList = await connectedAccounts.list(connectionsData); + + const connectionId = connectionsList.items[0].id; + + const connection = await connectedAccounts.get({ + connectedAccountId: connectionId, }); + // @ts-ignore + expect(connection.id).toBe(connectionId); + }); + it("should retrieve a specific connection for entity", async () => { + // @ts-ignore + const connectionsData: TConnectionData = { + user_uuid: "default", + }; + const connectionsList = await connectedAccounts.list(connectionsData); - it("should retrieve a specific connection", async () => { + const connectionId = connectionsList.items[0].id; - // @ts-ignore - const connectionsData: TConnectionData = { - appNames: 'github' - }; - const connectionsList = await connectedAccounts.list(connectionsData); - - const connectionId = connectionsList.items[0].id; - - const connection = await connectedAccounts.get({ connectedAccountId: connectionId }); - // @ts-ignore - expect(connection.id).toBe(connectionId); + const connection = await connectedAccounts.get({ + connectedAccountId: connectionId, }); - - it("should retrieve a specific connection for entity", async () => { - // @ts-ignore - const connectionsData: TConnectionData = { - user_uuid: 'default' - }; - const connectionsList = await connectedAccounts.list(connectionsData); - - const connectionId = connectionsList.items[0].id; - - const connection = await connectedAccounts.get({ connectedAccountId: connectionId }); - // @ts-ignore - expect(connection.id).toBe(connectionId); - }); - + // @ts-ignore + expect(connection.id).toBe(connectionId); + }); }); diff --git a/js/src/sdk/models/connectedAccounts.ts b/js/src/sdk/models/connectedAccounts.ts index 80dc5a93ad3..e0bdabf4509 100644 --- a/js/src/sdk/models/connectedAccounts.ts +++ b/js/src/sdk/models/connectedAccounts.ts @@ -1,182 +1,240 @@ - -import { InitiateConnectionPayloadDto, GetConnectionsResponseDto, GetConnectionInfoData, GetConnectionInfoResponse, GetConnectionsData } from "../client"; +import { + InitiateConnectionPayloadDto, + GetConnectionsResponseDto, + GetConnectionInfoData, + GetConnectionInfoResponse, + GetConnectionsData, +} from "../client"; import client from "../client/client"; -import apiClient from "../client/client" +import apiClient from "../client/client"; import { BackendClient } from "./backendClient"; import { Integrations } from "./integrations"; import { Apps } from "./apps"; import { CEG } from "../utils/error"; -type ConnectedAccountsListData = GetConnectionsData['query'] & { appNames?: string }; +type ConnectedAccountsListData = GetConnectionsData["query"] & { + appNames?: string; +}; type InitiateConnectionDataReq = InitiateConnectionPayloadDto & { - data?: Record | unknown; - entityId?: string; - labels?: string[]; - integrationId?: string; - redirectUri?: string; - authMode?: string; - authConfig?: { [key: string]: any }, - appName?: string; -} + data?: Record | unknown; + entityId?: string; + labels?: string[]; + integrationId?: string; + redirectUri?: string; + authMode?: string; + authConfig?: { [key: string]: any }; + appName?: string; +}; export class ConnectedAccounts { - backendClient: BackendClient; - integrations: Integrations; - apps: Apps; - - constructor(backendClient: BackendClient) { - this.backendClient = backendClient; - this.integrations = new Integrations(this.backendClient); - this.apps = new Apps(this.backendClient); + backendClient: BackendClient; + integrations: Integrations; + apps: Apps; + + constructor(backendClient: BackendClient) { + this.backendClient = backendClient; + this.integrations = new Integrations(this.backendClient); + this.apps = new Apps(this.backendClient); + } + + async list( + data: ConnectedAccountsListData + ): Promise { + try { + const res = await apiClient.connections.getConnections({ query: data }); + return res.data!; + } catch (error) { + throw CEG.handleAllError(error); } - - async list(data: ConnectedAccountsListData): Promise { - try { - const res = await apiClient.connections.getConnections({ query: data }); - return res.data!; - } catch (error) { - throw CEG.handleAllError(error); - } + } + + async create(data: InitiateConnectionPayloadDto) { + try { + const { data: res } = await apiClient.connections.initiateConnection({ + body: data, + }); + //@ts-ignore + return new ConnectionRequest( + res.connectionStatus, + res.connectedAccountId, + res.redirectUrl + ); + } catch (error) { + throw CEG.handleAllError(error); } - - async create(data: InitiateConnectionPayloadDto) { - try { - const { data: res } = await apiClient.connections.initiateConnection({ body: data }); - //@ts-ignore - return new ConnectionRequest(res.connectionStatus, res.connectedAccountId, res.redirectUrl); - } catch (error) { - throw CEG.handleAllError(error); - } + } + + async get(data: { connectedAccountId: string }) { + try { + const res = await apiClient.connections.getConnection({ path: data }); + return res.data; + } catch (error) { + throw CEG.handleAllError(error); } - - async get(data: { connectedAccountId: string }) { - try { - const res = await apiClient.connections.getConnection({ path: data }); - return res.data; - } catch (error) { - throw CEG.handleAllError(error); - } + } + + async delete(data: { connectedAccountId: string }) { + try { + const res = await apiClient.connections.deleteConnection({ path: data }); + return res.data; + } catch (error) { + throw CEG.handleAllError(error); } - - async delete(data: { connectedAccountId: string }) { - try { - const res = await apiClient.connections.deleteConnection({ path: data }); - return res.data; - } catch (error) { - throw CEG.handleAllError(error); - } + } + + async getAuthParams(data: { connectedAccountId: string }) { + try { + const res = await apiClient.connections.getConnection({ + path: { connectedAccountId: data.connectedAccountId }, + }); + return res.data; + } catch (error) { + throw CEG.handleAllError(error); } - - async getAuthParams(data: { connectedAccountId: string }) { - try { - const res = await apiClient.connections.getConnection({ path: { connectedAccountId: data.connectedAccountId } }); - return res.data; - } catch (error) { - throw CEG.handleAllError(error); - } - } - - async initiate(payload: InitiateConnectionDataReq): Promise { - try { - let { integrationId, entityId = 'default', labels, data = {}, redirectUri, authMode, authConfig, appName } = payload; - - if (!integrationId && authMode) { - const timestamp = new Date().toISOString().replace(/[-:.]/g, ""); - - if(!appName) throw new Error("appName is required when integrationId is not provided"); - if(!authMode) throw new Error("authMode is required when integrationId is not provided"); - if(!authConfig) throw new Error("authConfig is required when integrationId is not provided"); - - const app = await this.apps.get({ appKey: appName }); - const integration = await this.integrations.create({ - appId: app.appId!, - name: `integration_${timestamp}`, - authScheme: authMode, - authConfig: authConfig, - useComposioAuth: false, - }); - integrationId = integration?.id!; - } - - const res = await client.connections.initiateConnection({ - body: { - integrationId, - entityId, - labels, - redirectUri, - data, - } - }).then(res => res.data); - - return new ConnectionRequest({ - connectionStatus: res?.connectionStatus!, - connectedAccountId: res?.connectedAccountId!, - redirectUri: res?.redirectUrl! - }) - } catch (error) { - throw CEG.handleAllError(error); - } + } + + async initiate( + payload: InitiateConnectionDataReq + ): Promise { + try { + let { + integrationId, + entityId = "default", + labels, + data = {}, + redirectUri, + authMode, + authConfig, + appName, + } = payload; + + if (!integrationId && authMode) { + const timestamp = new Date().toISOString().replace(/[-:.]/g, ""); + + if (!appName) + throw new Error( + "appName is required when integrationId is not provided" + ); + if (!authMode) + throw new Error( + "authMode is required when integrationId is not provided" + ); + if (!authConfig) + throw new Error( + "authConfig is required when integrationId is not provided" + ); + + const app = await this.apps.get({ appKey: appName }); + const integration = await this.integrations.create({ + appId: app.appId!, + name: `integration_${timestamp}`, + authScheme: authMode, + authConfig: authConfig, + useComposioAuth: false, + }); + integrationId = integration?.id!; + } + + const res = await client.connections + .initiateConnection({ + body: { + integrationId, + entityId, + labels, + redirectUri, + data, + }, + }) + .then((res) => res.data); + + return new ConnectionRequest({ + connectionStatus: res?.connectionStatus!, + connectedAccountId: res?.connectedAccountId!, + redirectUri: res?.redirectUrl!, + }); + } catch (error) { + throw CEG.handleAllError(error); } + } } export class ConnectionRequest { + connectionStatus: string; + connectedAccountId: string; + redirectUrl: string | null; + + constructor({ + connectionStatus, + connectedAccountId, + redirectUri, + }: { connectionStatus: string; connectedAccountId: string; - redirectUrl: string | null; - - constructor({connectionStatus,connectedAccountId, redirectUri}: {connectionStatus: string,connectedAccountId: string, redirectUri: string | null}) { - this.connectionStatus = connectionStatus; - this.connectedAccountId = connectedAccountId; - this.redirectUrl = redirectUri; - } - - async saveUserAccessData(data: { - fieldInputs: Record; - redirectUrl?: string; - entityId?: string; - }) { - try { - const { data: connectedAccount } = await apiClient.connections.getConnection({ path: { connectedAccountId: this.connectedAccountId } }); - if (!connectedAccount) throw new Error("Connected account not found"); - return await apiClient.connections.initiateConnection({ - body: { - integrationId: connectedAccount.integrationId, - //@ts-ignore - data: data.fieldInputs, - redirectUri: data.redirectUrl, - userUuid: data.entityId, - entityId: data.entityId, - } - }); - } catch (error) { - throw CEG.handleAllError(error); - } + redirectUri: string | null; + }) { + this.connectionStatus = connectionStatus; + this.connectedAccountId = connectedAccountId; + this.redirectUrl = redirectUri; + } + + async saveUserAccessData(data: { + fieldInputs: Record; + redirectUrl?: string; + entityId?: string; + }) { + try { + const { data: connectedAccount } = + await apiClient.connections.getConnection({ + path: { connectedAccountId: this.connectedAccountId }, + }); + if (!connectedAccount) throw new Error("Connected account not found"); + return await apiClient.connections.initiateConnection({ + body: { + integrationId: connectedAccount.integrationId, + //@ts-ignore + data: data.fieldInputs, + redirectUri: data.redirectUrl, + userUuid: data.entityId, + entityId: data.entityId, + }, + }); + } catch (error) { + throw CEG.handleAllError(error); } - - async getAuthInfo(data: GetConnectionInfoData["path"]): Promise { - try { - const res = await client.connections.getConnectionInfo({ path: data }); - return res.data!; - } catch (error) { - throw CEG.handleAllError(error); - } + } + + async getAuthInfo( + data: GetConnectionInfoData["path"] + ): Promise { + try { + const res = await client.connections.getConnectionInfo({ path: data }); + return res.data!; + } catch (error) { + throw CEG.handleAllError(error); } - - async waitUntilActive(timeout = 60) { - try { - const startTime = Date.now(); - while (Date.now() - startTime < timeout * 1000) { - const connection = await apiClient.connections.getConnection({ path: { connectedAccountId: this.connectedAccountId } }).then(res => res.data); - if (!connection) throw new Error("Connected account not found"); - if (connection.status === 'ACTIVE') { - return connection; - } - await new Promise(resolve => setTimeout(resolve, 1000)); - } - throw new Error('Connection did not become active within the timeout period.'); - } catch (error) { - throw CEG.handleAllError(error); + } + + async waitUntilActive(timeout = 60) { + try { + const startTime = Date.now(); + while (Date.now() - startTime < timeout * 1000) { + const connection = await apiClient.connections + .getConnection({ + path: { connectedAccountId: this.connectedAccountId }, + }) + .then((res) => res.data); + if (!connection) throw new Error("Connected account not found"); + if (connection.status === "ACTIVE") { + return connection; } + await new Promise((resolve) => setTimeout(resolve, 1000)); + } + throw new Error( + "Connection did not become active within the timeout period." + ); + } catch (error) { + throw CEG.handleAllError(error); } -} \ No newline at end of file + } +} diff --git a/js/src/sdk/models/integrations.spec.ts b/js/src/sdk/models/integrations.spec.ts index cd9d957da76..d64e11eaf46 100644 --- a/js/src/sdk/models/integrations.spec.ts +++ b/js/src/sdk/models/integrations.spec.ts @@ -3,57 +3,58 @@ import { getBackendClient } from "../testUtils/getBackendClient"; import { Integrations } from "./integrations"; describe("Integrations class tests", () => { - let backendClient; - let integrations: Integrations; - let createdIntegrationId: string; - - beforeAll(() => { - backendClient = getBackendClient(); - integrations = new Integrations(backendClient); + let backendClient; + let integrations: Integrations; + let createdIntegrationId: string; + + beforeAll(() => { + backendClient = getBackendClient(); + integrations = new Integrations(backendClient); + }); + + it("Retrieve integrations list", async () => { + const integrationsList = await integrations.list(); + expect(integrationsList?.items).toBeInstanceOf(Array); + expect(integrationsList?.items).not.toHaveLength(0); + }); + + it("should create an integration and verify its properties", async () => { + const integrationCreation = await integrations.create({ + appId: "01e22f33-dc3f-46ae-b58d-050e4d2d1909", + name: "test_integration_220", + useComposioAuth: true, + // @ts-ignore + forceNewIntegration: true, }); + expect(integrationCreation.id).toBeTruthy(); + expect(integrationCreation.appName).toBe("github"); - it("Retrieve integrations list", async () => { - const integrationsList = await integrations.list(); - expect(integrationsList?.items).toBeInstanceOf(Array); - expect(integrationsList?.items).not.toHaveLength(0); - }); - - it("should create an integration and verify its properties", async () => { - const integrationCreation = await integrations.create({ - appId: "01e22f33-dc3f-46ae-b58d-050e4d2d1909", - name: "test_integration_220", - useComposioAuth: true, - // @ts-ignore - forceNewIntegration:true - }); - expect(integrationCreation.id).toBeTruthy(); - expect(integrationCreation.appName).toBe("github"); - - // @ts-ignore - createdIntegrationId = integrationCreation.id; - }); - - it("should retrieve the created integration by ID and verify its properties", async () => { - const integration = await integrations.get({ - integrationId: createdIntegrationId - }); - expect(integration?.id).toBe(createdIntegrationId); - expect(integration?.appId).toBe("01e22f33-dc3f-46ae-b58d-050e4d2d1909"); - expect(integration?.authScheme).toBe("OAUTH2"); - expect(integration?.expectedInputFields).toBeDefined(); - }); + // @ts-ignore + createdIntegrationId = integrationCreation.id; + }); - it("should get the required params for the created integration", async () => { - const requiredParams = await integrations.getRequiredParams(createdIntegrationId); - expect(requiredParams).toBeDefined(); + it("should retrieve the created integration by ID and verify its properties", async () => { + const integration = await integrations.get({ + integrationId: createdIntegrationId, }); - - it("should delete the created integration", async () => { - if (!createdIntegrationId) return; - await integrations.delete({ - path:{ - integrationId: createdIntegrationId - } - }); + expect(integration?.id).toBe(createdIntegrationId); + expect(integration?.appId).toBe("01e22f33-dc3f-46ae-b58d-050e4d2d1909"); + expect(integration?.authScheme).toBe("OAUTH2"); + expect(integration?.expectedInputFields).toBeDefined(); + }); + + it("should get the required params for the created integration", async () => { + const requiredParams = + await integrations.getRequiredParams(createdIntegrationId); + expect(requiredParams).toBeDefined(); + }); + + it("should delete the created integration", async () => { + if (!createdIntegrationId) return; + await integrations.delete({ + path: { + integrationId: createdIntegrationId, + }, }); + }); }); diff --git a/js/src/sdk/models/integrations.ts b/js/src/sdk/models/integrations.ts index c285598d127..f731c684533 100644 --- a/js/src/sdk/models/integrations.ts +++ b/js/src/sdk/models/integrations.ts @@ -1,189 +1,191 @@ -import { DeleteConnectorData, GetConnectorInfoData, GetConnectorInfoResDTO, GetConnectorListResDTO } from "../client"; -import apiClient from "../client/client" +import { + DeleteConnectorData, + GetConnectorInfoData, + GetConnectorInfoResDTO, + GetConnectorListResDTO, +} from "../client"; +import apiClient from "../client/client"; import { BackendClient } from "./backendClient"; import { CEG } from "../utils/error"; export type ListAllIntegrationsData = { + /** + * Page number to fetch + */ + page?: number; + /** + * Page Size to assume + */ + pageSize?: number; + /** + * The name of the app to filter by + */ + appName?: string; + /** + * Whether to show disabled integrations + */ + showDisabled?: boolean; +}; + +export type GetIntegrationData = { + /** + * The unique identifier of the integration. + */ + integrationId: string; +}; + +export type CreateIntegrationData = { + requestBody?: { /** - * Page number to fetch + * The name of the connector. */ - page?: number; + name?: string; /** - * Page Size to assume + * The authentication scheme used by the connector (e.g., "OAUTH2", "API_KEY"). */ - pageSize?: number; + authScheme?: string; /** - * The name of the app to filter by + * The unique identifier of the app associated with the connector. */ - appName?: string; + appId?: string; + forceNewIntegration?: boolean; /** - * Whether to show disabled integrations + * An object containing the authentication configuration for the connector. */ - showDisabled?: boolean; -}; + authConfig?: { + /** + * The client ID used for authentication with the app - if authScheme is OAUTH2 + */ + client_id?: string; + /** + * The client secret used for authentication with the app - if authScheme is OAUTH2 + */ + client_secret?: string; + /** + * The API key used for authentication with the app - if authScheme is API_KEY + */ + api_key?: string; + /** + * The Consumer key used for authentication with the app - if authScheme is OAUTH1 + */ + consumer_key?: string; + /** + * The Consumer secret used for authentication with the app - if authScheme is OAUTH1 + */ + consumer_secret?: string; + /** + * The base URL for making API requests to the app. + */ + base_url?: string; -export type GetIntegrationData = { + [key: string]: unknown; + }; /** - * The unique identifier of the integration. + * Use default Composio credentials to proceed. The developer app credentials will be of Composio. */ - integrationId: string; + useComposioAuth?: boolean; + }; }; -export type CreateIntegrationData = { - requestBody?: { - /** - * The name of the connector. - */ - name?: string; - /** - * The authentication scheme used by the connector (e.g., "OAUTH2", "API_KEY"). - */ - authScheme?: string; - /** - * The unique identifier of the app associated with the connector. - */ - appId?: string; - forceNewIntegration?: boolean; - /** - * An object containing the authentication configuration for the connector. - */ - authConfig?: { - /** - * The client ID used for authentication with the app - if authScheme is OAUTH2 - */ - client_id?: string; - /** - * The client secret used for authentication with the app - if authScheme is OAUTH2 - */ - client_secret?: string; - /** - * The API key used for authentication with the app - if authScheme is API_KEY - */ - api_key?: string; - /** - * The Consumer key used for authentication with the app - if authScheme is OAUTH1 - */ - consumer_key?: string; - /** - * The Consumer secret used for authentication with the app - if authScheme is OAUTH1 - */ - consumer_secret?: string; - /** - * The base URL for making API requests to the app. - */ - base_url?: string; - - [key: string]: unknown; - }; - /** - * Use default Composio credentials to proceed. The developer app credentials will be of Composio. - */ - useComposioAuth?: boolean; - }; -}; - - export class Integrations { + backendClient: BackendClient; - backendClient: BackendClient; + constructor(backendClient: BackendClient) { + this.backendClient = backendClient; + } - constructor(backendClient: BackendClient) { - this.backendClient = backendClient; - } + /** + * Retrieves a list of all available integrations in the Composio platform. + * + * This method allows clients to explore and discover the supported integrations. It returns an array of integration objects, each containing essential details such as the integration's key, name, description, logo, categories, and unique identifier. + * + * @returns {Promise} A promise that resolves to the list of all integrations. + * @throws {ApiError} If the request fails. + */ + async list(data: ListAllIntegrationsData = {}) { + try { + const response = await apiClient.appConnector.listAllConnectors({ + query: data, + }); - /** - * Retrieves a list of all available integrations in the Composio platform. - * - * This method allows clients to explore and discover the supported integrations. It returns an array of integration objects, each containing essential details such as the integration's key, name, description, logo, categories, and unique identifier. - * - * @returns {Promise} A promise that resolves to the list of all integrations. - * @throws {ApiError} If the request fails. - */ - async list(data: ListAllIntegrationsData = {}) { - try { - const response = await apiClient.appConnector.listAllConnectors({ - query: data - }); - - - return response.data - } catch (error) { - throw CEG.handleAllError(error); - } + return response.data; + } catch (error) { + throw CEG.handleAllError(error); } + } - /** - * Retrieves details of a specific integration in the Composio platform by providing its integration name. - * - * The response includes the integration's name, display name, description, input parameters, expected response, associated app information, and enabled status. - * - * @param {GetIntegrationData} data The data for the request. - * @returns {Promise} A promise that resolves to the details of the integration. - * @throws {ApiError} If the request fails. - */ - async get(data: GetIntegrationData) { - try { - const response = await apiClient.appConnector.getConnectorInfo({ - path: data - }); - return response.data; - } catch (error) { - throw CEG.handleAllError(error); - } + /** + * Retrieves details of a specific integration in the Composio platform by providing its integration name. + * + * The response includes the integration's name, display name, description, input parameters, expected response, associated app information, and enabled status. + * + * @param {GetIntegrationData} data The data for the request. + * @returns {Promise} A promise that resolves to the details of the integration. + * @throws {ApiError} If the request fails. + */ + async get(data: GetIntegrationData) { + try { + const response = await apiClient.appConnector.getConnectorInfo({ + path: data, + }); + return response.data; + } catch (error) { + throw CEG.handleAllError(error); } + } - async getRequiredParams(integrationId: string) { - try { - const response = await apiClient.appConnector.getConnectorInfo({ - path: { - integrationId - }, - throwOnError: true - }); - return response.data?.expectedInputFields; - } catch (error) { - throw CEG.handleAllError(error); - } + async getRequiredParams(integrationId: string) { + try { + const response = await apiClient.appConnector.getConnectorInfo({ + path: { + integrationId, + }, + throwOnError: true, + }); + return response.data?.expectedInputFields; + } catch (error) { + throw CEG.handleAllError(error); } + } - /** - * Creates a new integration in the Composio platform. - * - * This method allows clients to create a new integration by providing the necessary details such as app ID, name, authentication mode, and configuration. - * - * @param {CreateIntegrationData["requestBody"]} data The data for the request. - * @returns {Promise} A promise that resolves to the created integration model. - * @throws {ApiError} If the request fails. - */ - async create(data: CreateIntegrationData["requestBody"]) { - try { - if (!data?.authConfig) { - data!.authConfig = {}; - } + /** + * Creates a new integration in the Composio platform. + * + * This method allows clients to create a new integration by providing the necessary details such as app ID, name, authentication mode, and configuration. + * + * @param {CreateIntegrationData["requestBody"]} data The data for the request. + * @returns {Promise} A promise that resolves to the created integration model. + * @throws {ApiError} If the request fails. + */ + async create(data: CreateIntegrationData["requestBody"]) { + try { + if (!data?.authConfig) { + data!.authConfig = {}; + } - const response = await apiClient.appConnector.createConnector({ - body: { - name: data?.name!, - appId: data?.appId!, - authConfig: data?.authConfig! as any, - authScheme: data?.authScheme, - useComposioAuth: data?.useComposioAuth!, - forceNewIntegration: true - }, - throwOnError: true - }); - return response.data; - } catch (error) { - throw CEG.handleAllError(error); - } + const response = await apiClient.appConnector.createConnector({ + body: { + name: data?.name!, + appId: data?.appId!, + authConfig: data?.authConfig! as any, + authScheme: data?.authScheme, + useComposioAuth: data?.useComposioAuth!, + forceNewIntegration: true, + }, + throwOnError: true, + }); + return response.data; + } catch (error) { + throw CEG.handleAllError(error); } + } - async delete(data: DeleteConnectorData) { - try { - const response = await apiClient.appConnector.deleteConnector(data); - return response.data; - } catch (error) { - throw CEG.handleAllError(error); - } - } -} \ No newline at end of file + async delete(data: DeleteConnectorData) { + try { + const response = await apiClient.appConnector.deleteConnector(data); + return response.data; + } catch (error) { + throw CEG.handleAllError(error); + } + } +} diff --git a/js/src/sdk/models/triggers.spec.ts b/js/src/sdk/models/triggers.spec.ts index bf98f47b094..1ebbb1fb595 100644 --- a/js/src/sdk/models/triggers.spec.ts +++ b/js/src/sdk/models/triggers.spec.ts @@ -1,4 +1,3 @@ - import { describe, it, expect, beforeAll } from "@jest/globals"; import { getBackendClient } from "../testUtils/getBackendClient"; @@ -8,128 +7,130 @@ import { Entity } from "./Entity"; import { Actions } from "./actions"; describe("Apps class tests", () => { - let backendClient; - let triggers: Triggers; - let connectedAccounts: ConnectedAccounts; - let entity: Entity; - - let triggerId: string; - let actions: Actions; - - beforeAll(() => { - backendClient = getBackendClient(); - triggers = new Triggers(backendClient); - connectedAccounts = new ConnectedAccounts(backendClient); - entity = new Entity(backendClient, "default"); - connectedAccounts = new ConnectedAccounts(backendClient); - actions = new Actions(backendClient); - }); - - it("should create an Apps instance and retrieve apps list", async () => { - const triggerList = await triggers.list(); - expect(triggerList.length).toBeGreaterThan(0); - }); - - it("should retrieve a list of triggers for a specific app", async () => { - const triggerList = await triggers.list({ - appNames: "github" - }); - // this is breaking for now - expect(triggerList.length).toBeGreaterThan(0); - expect(triggerList[0].appName).toBe("github"); + let backendClient; + let triggers: Triggers; + let connectedAccounts: ConnectedAccounts; + let entity: Entity; + + let triggerId: string; + let actions: Actions; + + beforeAll(() => { + backendClient = getBackendClient(); + triggers = new Triggers(backendClient); + connectedAccounts = new ConnectedAccounts(backendClient); + entity = new Entity(backendClient, "default"); + connectedAccounts = new ConnectedAccounts(backendClient); + actions = new Actions(backendClient); + }); + + it("should create an Apps instance and retrieve apps list", async () => { + const triggerList = await triggers.list(); + expect(triggerList.length).toBeGreaterThan(0); + }); + + it("should retrieve a list of triggers for a specific app", async () => { + const triggerList = await triggers.list({ + appNames: "github", }); - - + // this is breaking for now + expect(triggerList.length).toBeGreaterThan(0); + expect(triggerList[0].appName).toBe("github"); + }); }); - describe("Apps class tests subscribe", () => { - let backendClient; - let triggers: Triggers; - let connectedAccounts: ConnectedAccounts; - let actions: Actions; - let entity: Entity; - - let triggerId: string; - - beforeAll(() => { - backendClient = getBackendClient(); - triggers = new Triggers(backendClient); - connectedAccounts = new ConnectedAccounts(backendClient); - entity = new Entity(backendClient, "default"); - actions = new Actions(backendClient); + let backendClient; + let triggers: Triggers; + let connectedAccounts: ConnectedAccounts; + let actions: Actions; + let entity: Entity; + + let triggerId: string; + + beforeAll(() => { + backendClient = getBackendClient(); + triggers = new Triggers(backendClient); + connectedAccounts = new ConnectedAccounts(backendClient); + entity = new Entity(backendClient, "default"); + actions = new Actions(backendClient); + }); + + it("should create a new trigger for gmail", async () => { + const connectedAccount = await connectedAccounts.list({ + user_uuid: "default", }); - - - it("should create a new trigger for gmail", async () => { - const connectedAccount = await connectedAccounts.list({ user_uuid: 'default' }); - - const connectedAccountId = connectedAccount.items.find(item => item.appName === 'gmail')?.id; - if(!connectedAccountId) { - throw new Error("No connected account found"); - } - const trigger = await triggers.setup({connectedAccountId, triggerName: 'gmail_new_gmail_message', config: { - "userId": connectedAccount.items[0].id, - "interval": 60, - "labelIds": "INBOX", - }}); - - expect(trigger.status).toBe("success"); - expect(trigger.triggerId).toBeTruthy(); - - triggerId = trigger.triggerId; - }) - - it("should disable, enable, and then disable the created trigger", async () => { - let trigger = await triggers.disable({ triggerId }); - expect(trigger.status).toBe("success"); - - trigger = await triggers.enable({ triggerId }); - expect(trigger.status).toBe("success"); - - trigger = await triggers.disable({ triggerId }); - expect(trigger.status).toBe("success"); + const connectedAccountId = connectedAccount.items.find( + (item) => item.appName === "gmail" + )?.id; + if (!connectedAccountId) { + throw new Error("No connected account found"); + } + const trigger = await triggers.setup({ + connectedAccountId, + triggerName: "gmail_new_gmail_message", + config: { + userId: connectedAccount.items[0].id, + interval: 60, + labelIds: "INBOX", + }, }); - // it("should subscribe to a trigger and receive a trigger", async () => { - // function waitForTriggerReceived() { - // return new Promise((resolve) => { - // triggers.subscribe((data) => { - // resolve(data); - // }, { - // appName: "github", - // triggerName: "GITHUB_ISSUE_ADDED_EVENT" - // }); - - // setTimeout(async () => { - // const actionName = "github_create_an_issue"; - // // Not urgent - // const connectedAccountsResult = await connectedAccounts.list({ integrationId: 'ca85b86b-1198-4e1a-8d84-b14640564c77' }); - // const connectionId = connectedAccountsResult.items[0].id; - - // await actions.execute({ - // actionName, - // requestBody: { - // connectedAccountId: connectionId, - // input: { - // title: "test", - // owner: "ComposioHQ", - // repo: "test_repo", - // }, - // appName: 'github' - // } - // }); - // }, 4000); - // }); - // } - - // const data = await waitForTriggerReceived(); - - // //@ts-ignore - // expect(data.payload.triggerName).toBe("GITHUB_ISSUE_ADDED_EVENT"); - - // triggers.unsubscribe(); - // }); - + expect(trigger.status).toBe("success"); + expect(trigger.triggerId).toBeTruthy(); + + triggerId = trigger.triggerId; + }); + + it("should disable, enable, and then disable the created trigger", async () => { + let trigger = await triggers.disable({ triggerId }); + expect(trigger.status).toBe("success"); + + trigger = await triggers.enable({ triggerId }); + expect(trigger.status).toBe("success"); + + trigger = await triggers.disable({ triggerId }); + expect(trigger.status).toBe("success"); + }); + + // it("should subscribe to a trigger and receive a trigger", async () => { + // function waitForTriggerReceived() { + // return new Promise((resolve) => { + // triggers.subscribe((data) => { + // resolve(data); + // }, { + // appName: "github", + // triggerName: "GITHUB_ISSUE_ADDED_EVENT" + // }); + + // setTimeout(async () => { + // const actionName = "github_create_an_issue"; + // // Not urgent + // const connectedAccountsResult = await connectedAccounts.list({ integrationId: 'ca85b86b-1198-4e1a-8d84-b14640564c77' }); + // const connectionId = connectedAccountsResult.items[0].id; + + // await actions.execute({ + // actionName, + // requestBody: { + // connectedAccountId: connectionId, + // input: { + // title: "test", + // owner: "ComposioHQ", + // repo: "test_repo", + // }, + // appName: 'github' + // } + // }); + // }, 4000); + // }); + // } + + // const data = await waitForTriggerReceived(); + + // //@ts-ignore + // expect(data.payload.triggerName).toBe("GITHUB_ISSUE_ADDED_EVENT"); + + // triggers.unsubscribe(); + // }); }); diff --git a/js/src/sdk/models/triggers.ts b/js/src/sdk/models/triggers.ts index c3a3caad768..25599b36f50 100644 --- a/js/src/sdk/models/triggers.ts +++ b/js/src/sdk/models/triggers.ts @@ -1,9 +1,8 @@ - import { TriggerData, PusherUtils } from "../utils/pusher"; import logger from "../../utils/logger"; -import {BackendClient} from "./backendClient" +import { BackendClient } from "./backendClient"; -import apiClient from "../client/client" +import apiClient from "../client/client"; import { CEG } from "../utils/error"; import { ListTriggersData } from "../client"; @@ -11,145 +10,169 @@ import { ListTriggersData } from "../client"; type RequiredQuery = ListTriggersData["query"]; export class Triggers { - trigger_to_client_event = "trigger_to_client"; + trigger_to_client_event = "trigger_to_client"; - backendClient: BackendClient; - constructor(backendClient: BackendClient) { - this.backendClient = backendClient; - } + backendClient: BackendClient; + constructor(backendClient: BackendClient) { + this.backendClient = backendClient; + } - /** - * Retrieves a list of all triggers in the Composio platform. - * - * This method allows you to fetch a list of all the available triggers. It supports pagination to handle large numbers of triggers. The response includes an array of trigger objects, each containing information such as the trigger's name, description, input parameters, expected response, associated app information, and enabled status. - * - * @param {ListTriggersData} data The data for the request. - * @returns {CancelablePromise} A promise that resolves to the list of all triggers. - * @throws {ApiError} If the request fails. - */ - async list(data: RequiredQuery={} ) { - try { - const {data:response} = await apiClient.triggers.listTriggers({ - query: { - appNames: data?.appNames, - } - }); - return response || []; - } catch (error) { - throw CEG.handleAllError(error); - } + /** + * Retrieves a list of all triggers in the Composio platform. + * + * This method allows you to fetch a list of all the available triggers. It supports pagination to handle large numbers of triggers. The response includes an array of trigger objects, each containing information such as the trigger's name, description, input parameters, expected response, associated app information, and enabled status. + * + * @param {ListTriggersData} data The data for the request. + * @returns {CancelablePromise} A promise that resolves to the list of all triggers. + * @throws {ApiError} If the request fails. + */ + async list(data: RequiredQuery = {}) { + try { + const { data: response } = await apiClient.triggers.listTriggers({ + query: { + appNames: data?.appNames, + }, + }); + return response || []; + } catch (error) { + throw CEG.handleAllError(error); } + } - /** - * Setup a trigger for a connected account. - * - * @param {SetupTriggerData} data The data for the request. - * @returns {CancelablePromise} A promise that resolves to the setup trigger response. - * @throws {ApiError} If the request fails. - */ - async setup({connectedAccountId, triggerName, config}: {connectedAccountId: string, triggerName: string, config: Record}): Promise<{status: string, triggerId: string}> { - try { - const response = await apiClient.triggers.enableTrigger({ - path: { - connectedAccountId, - triggerName - }, - body: { - triggerConfig: config - } - }); - return response.data as {status: string, triggerId: string}; - } catch (error) { - throw CEG.handleAllError(error); - } + /** + * Setup a trigger for a connected account. + * + * @param {SetupTriggerData} data The data for the request. + * @returns {CancelablePromise} A promise that resolves to the setup trigger response. + * @throws {ApiError} If the request fails. + */ + async setup({ + connectedAccountId, + triggerName, + config, + }: { + connectedAccountId: string; + triggerName: string; + config: Record; + }): Promise<{ status: string; triggerId: string }> { + try { + const response = await apiClient.triggers.enableTrigger({ + path: { + connectedAccountId, + triggerName, + }, + body: { + triggerConfig: config, + }, + }); + return response.data as { status: string; triggerId: string }; + } catch (error) { + throw CEG.handleAllError(error); } + } - async enable(data: { triggerId: string }) { - try { - const response = await apiClient.triggers.switchTriggerInstanceStatus({ - path: data, - body: { - enabled: true - } - }); - return { - status: "success" - } - } catch (error) { - throw CEG.handleAllError(error); - } + async enable(data: { triggerId: string }) { + try { + const response = await apiClient.triggers.switchTriggerInstanceStatus({ + path: data, + body: { + enabled: true, + }, + }); + return { + status: "success", + }; + } catch (error) { + throw CEG.handleAllError(error); } + } - async disable(data: { triggerId: string }) { - try { - const response = await apiClient.triggers.switchTriggerInstanceStatus({ - path: data, - body: { - enabled: false - } - }); - return { - status: "success" - } - } catch (error) { - throw CEG.handleAllError(error); - } + async disable(data: { triggerId: string }) { + try { + const response = await apiClient.triggers.switchTriggerInstanceStatus({ + path: data, + body: { + enabled: false, + }, + }); + return { + status: "success", + }; + } catch (error) { + throw CEG.handleAllError(error); } + } - async delete(data: { triggerInstanceId: string }) { - try { - const response = await apiClient.triggers.deleteTrigger({ - path: data - }); - return { - status: "success" - } - } catch (error) { - throw CEG.handleAllError(error); - } + async delete(data: { triggerInstanceId: string }) { + try { + const response = await apiClient.triggers.deleteTrigger({ + path: data, + }); + return { + status: "success", + }; + } catch (error) { + throw CEG.handleAllError(error); } + } - async subscribe(fn: (data: TriggerData) => void, filters:{ - appName?: string, - triggerId? : string; - connectionId?: string; - integrationId?: string; - triggerName?: string; - triggerData?: string; - entityId?: string; - }={}) { + async subscribe( + fn: (data: TriggerData) => void, + filters: { + appName?: string; + triggerId?: string; + connectionId?: string; + integrationId?: string; + triggerName?: string; + triggerData?: string; + entityId?: string; + } = {} + ) { + if (!fn) throw new Error("Function is required for trigger subscription"); + //@ts-ignore + const clientId = await this.backendClient.getClientId(); + //@ts-ignore + await PusherUtils.getPusherClient( + this.backendClient.baseUrl, + this.backendClient.apiKey + ); - if(!fn) throw new Error("Function is required for trigger subscription"); - //@ts-ignore - const clientId = await this.backendClient.getClientId(); - //@ts-ignore - await PusherUtils.getPusherClient(this.backendClient.baseUrl, this.backendClient.apiKey); + const shouldSendTrigger = (data: TriggerData) => { + if (Object.keys(filters).length === 0) return true; + else { + return ( + (!filters.appName || + data.appName.toLowerCase() === filters.appName.toLowerCase()) && + (!filters.triggerId || + data.metadata.id.toLowerCase() === + filters.triggerId.toLowerCase()) && + (!filters.connectionId || + data.metadata.connectionId.toLowerCase() === + filters.connectionId.toLowerCase()) && + (!filters.triggerName || + data.metadata.triggerName.toLowerCase() === + filters.triggerName.toLowerCase()) && + (!filters.entityId || + data.metadata.connection.clientUniqueUserId.toLowerCase() === + filters.entityId.toLowerCase()) && + (!filters.integrationId || + data.metadata.connection.integrationId.toLowerCase() === + filters.integrationId.toLowerCase()) + ); + } + }; - const shouldSendTrigger = (data: TriggerData) => { - if(Object.keys(filters).length === 0) return true; - else{ - return ( - (!filters.appName || data.appName.toLowerCase() === filters.appName.toLowerCase()) && - (!filters.triggerId || data.metadata.id.toLowerCase() === filters.triggerId.toLowerCase()) && - (!filters.connectionId || data.metadata.connectionId.toLowerCase() === filters.connectionId.toLowerCase()) && - (!filters.triggerName || data.metadata.triggerName.toLowerCase() === filters.triggerName.toLowerCase()) && - (!filters.entityId || data.metadata.connection.clientUniqueUserId.toLowerCase() === filters.entityId.toLowerCase()) && - (!filters.integrationId || data.metadata.connection.integrationId.toLowerCase() === filters.integrationId.toLowerCase()) - ); - } - } - - logger.debug("Subscribing to triggers",filters) - PusherUtils.triggerSubscribe(clientId, (data: TriggerData) => { - if (shouldSendTrigger(data)) { - fn(data); - } - }); - } + logger.debug("Subscribing to triggers", filters); + PusherUtils.triggerSubscribe(clientId, (data: TriggerData) => { + if (shouldSendTrigger(data)) { + fn(data); + } + }); + } - async unsubscribe() { - //@ts-ignore - const clientId = await this.backendClient.getClientId(); - PusherUtils.triggerUnsubscribe(clientId); - } + async unsubscribe() { + //@ts-ignore + const clientId = await this.backendClient.getClientId(); + PusherUtils.triggerUnsubscribe(clientId); + } } diff --git a/js/src/sdk/utils/base/batchProcessor.ts b/js/src/sdk/utils/base/batchProcessor.ts index 434bd69322a..dfbff43ce33 100644 --- a/js/src/sdk/utils/base/batchProcessor.ts +++ b/js/src/sdk/utils/base/batchProcessor.ts @@ -1,35 +1,38 @@ - export class BatchProcessor { - private batch: unknown[] = []; - private time: number; - private batchSize: number; - private processBatchCallback: (data: unknown[]) => void; - private timer: NodeJS.Timeout | null = null; + private batch: unknown[] = []; + private time: number; + private batchSize: number; + private processBatchCallback: (data: unknown[]) => void; + private timer: NodeJS.Timeout | null = null; - constructor(time: number = 2000, batchSize: number = 100, processBatchCallback: (data: unknown[]) => void) { - this.batch = []; - this.time = time; - this.batchSize = batchSize; - this.processBatchCallback = processBatchCallback; - } + constructor( + time: number = 2000, + batchSize: number = 100, + processBatchCallback: (data: unknown[]) => void + ) { + this.batch = []; + this.time = time; + this.batchSize = batchSize; + this.processBatchCallback = processBatchCallback; + } - pushItem(item: unknown) { - this.batch.push(item); - if (this.batch.length >= this.batchSize) { - this.processBatch(); - } else if (!this.timer) { - this.timer = setTimeout(() => this.processBatch(), this.time); - } + pushItem(item: unknown) { + this.batch.push(item); + if (this.batch.length >= this.batchSize) { + this.processBatch(); + } else if (!this.timer) { + this.timer = setTimeout(() => this.processBatch(), this.time); } + } - processBatch() { - if (this.batch.length > 0) { - this.processBatchCallback(this.batch); - this.batch = []; - } - if (this.timer) { - clearTimeout(this.timer); - this.timer = null; - } + processBatch() { + if (this.batch.length > 0) { + this.processBatchCallback(this.batch); + this.batch = []; + } + if (this.timer) { + clearTimeout(this.timer); + this.timer = null; } -}; \ No newline at end of file + } +} diff --git a/js/src/sdk/utils/common.ts b/js/src/sdk/utils/common.ts index bf52d588e39..06c1b4d883a 100644 --- a/js/src/sdk/utils/common.ts +++ b/js/src/sdk/utils/common.ts @@ -1,4 +1,6 @@ // Helper function to stringify objects if needed -export const serializeValue = (obj: Record | string | number | boolean | null | undefined) => { - return typeof obj === 'object' ? JSON.stringify(obj) : obj; -} +export const serializeValue = ( + obj: Record | string | number | boolean | null | undefined +) => { + return typeof obj === "object" ? JSON.stringify(obj) : obj; +}; diff --git a/js/src/sdk/utils/composioContext.ts b/js/src/sdk/utils/composioContext.ts index c64a0453af3..c58068f018e 100644 --- a/js/src/sdk/utils/composioContext.ts +++ b/js/src/sdk/utils/composioContext.ts @@ -6,11 +6,11 @@ Warning: Can cause problems if there are multiple instances of the SDK running in the same process. */ class ComposioSDKContext { - static apiKey: string; - static baseURL: string; - static frameworkRuntime?: string; - static source?: string = "javascript"; - static composioVersion?: string; + static apiKey: string; + static baseURL: string; + static frameworkRuntime?: string; + static source?: string = "javascript"; + static composioVersion?: string; } -export default ComposioSDKContext; \ No newline at end of file +export default ComposioSDKContext; diff --git a/js/src/sdk/utils/config.ts b/js/src/sdk/utils/config.ts index da2b0f9a07f..e19dd7eeddb 100644 --- a/js/src/sdk/utils/config.ts +++ b/js/src/sdk/utils/config.ts @@ -1,111 +1,129 @@ -import * as fs from 'fs'; -import * as path from 'path'; -import * as os from 'os'; -import { LOCAL_CACHE_DIRECTORY_NAME, USER_DATA_FILE_NAME, DEFAULT_BASE_URL } from './constants'; +import * as fs from "fs"; +import * as path from "path"; +import * as os from "os"; +import { + LOCAL_CACHE_DIRECTORY_NAME, + USER_DATA_FILE_NAME, + DEFAULT_BASE_URL, +} from "./constants"; import { getEnvVariable } from "../../utils/shared"; import { client as axiosClient } from "../client/services.gen"; import apiClient from "../client/client"; -import { AxiosInstance } from 'axios'; -import logger from '../../utils/logger'; +import { AxiosInstance } from "axios"; +import logger from "../../utils/logger"; -declare module 'axios' { - export interface InternalAxiosRequestConfig { - metadata?: { - startTime?: number; - }; - } +declare module "axios" { + export interface InternalAxiosRequestConfig { + metadata?: { + startTime?: number; + }; + } } // File path helpers -export const userDataPath = () => path.join(os.homedir(), LOCAL_CACHE_DIRECTORY_NAME, USER_DATA_FILE_NAME); +export const userDataPath = () => + path.join(os.homedir(), LOCAL_CACHE_DIRECTORY_NAME, USER_DATA_FILE_NAME); export const getUserDataJson = () => { - try { - const data = fs.readFileSync(userDataPath(), 'utf8'); - return JSON.parse(data); - } catch (error: any) { - return {}; - } -} + try { + const data = fs.readFileSync(userDataPath(), "utf8"); + return JSON.parse(data); + } catch (error: any) { + return {}; + } +}; // Axios configuration export const setAxiosClientConfig = (axiosClientInstance: AxiosInstance) => { - axiosClientInstance.interceptors.request.use((request) => { - const body = request.data ? JSON.stringify(request.data) : ''; - logger.debug(`API Req [${request.method?.toUpperCase()}] ${request.url}`, { - ...(body && { body }) - }); - request.metadata = { startTime: Date.now() }; - return request; + axiosClientInstance.interceptors.request.use((request) => { + const body = request.data ? JSON.stringify(request.data) : ""; + logger.debug(`API Req [${request.method?.toUpperCase()}] ${request.url}`, { + ...(body && { body }), }); + request.metadata = { startTime: Date.now() }; + return request; + }); - axiosClientInstance.interceptors.response.use( - (response) => { - const method = response.config.method?.toUpperCase(); - const responseSize = Math.round(JSON.stringify(response.data).length / 1024); - const requestStartTime = response.config.metadata?.startTime; - const responseTime = requestStartTime ? Date.now() - requestStartTime : 0; - const responseData = response.data ? JSON.stringify(response.data) : ''; - const status = response.status; + axiosClientInstance.interceptors.response.use( + (response) => { + const method = response.config.method?.toUpperCase(); + const responseSize = Math.round( + JSON.stringify(response.data).length / 1024 + ); + const requestStartTime = response.config.metadata?.startTime; + const responseTime = requestStartTime ? Date.now() - requestStartTime : 0; + const responseData = response.data ? JSON.stringify(response.data) : ""; + const status = response.status; - // @ts-expect-error - response["metadata"] = { - responseTime, - responseSize - } - logger.debug(`API Res [${method}] ${response.config.url} - ${status} - ${responseSize} KB ${responseTime}ms`, { - ...(responseData && { response: JSON.parse(responseData) }) - }); - return response; - }, - (error) => { - const requestStartTime = error.config?.metadata?.startTime; - const responseTime = requestStartTime ? Date.now() - requestStartTime : 0; - const status = error.response?.status || 'Unknown'; - const length = JSON.stringify(error.response?.data)?.length || 0; - const responseSize = Math.round(length / 1024); - - error["metadata"] = { - responseTime, - responseSize - } - logger.debug(`API Error [${status}] ${error.config?.method?.toUpperCase()} ${error.config?.url} - ${status} - ${responseTime}ms`, { - headers: error.response?.headers, - data: error.response?.data, - error: error.message, - responseTime - }); - return Promise.reject(error); + // @ts-expect-error + response["metadata"] = { + responseTime, + responseSize, + }; + logger.debug( + `API Res [${method}] ${response.config.url} - ${status} - ${responseSize} KB ${responseTime}ms`, + { + ...(responseData && { response: JSON.parse(responseData) }), } - ); -} + ); + return response; + }, + (error) => { + const requestStartTime = error.config?.metadata?.startTime; + const responseTime = requestStartTime ? Date.now() - requestStartTime : 0; + const status = error.response?.status || "Unknown"; + const length = JSON.stringify(error.response?.data)?.length || 0; + const responseSize = Math.round(length / 1024); + + error["metadata"] = { + responseTime, + responseSize, + }; + logger.debug( + `API Error [${status}] ${error.config?.method?.toUpperCase()} ${error.config?.url} - ${status} - ${responseTime}ms`, + { + headers: error.response?.headers, + data: error.response?.data, + error: error.message, + responseTime, + } + ); + return Promise.reject(error); + } + ); +}; // Client configuration functions export function getSDKConfig(baseUrl?: string, apiKey?: string) { - const userData = getUserDataJson(); - const { api_key: apiKeyFromUserConfig, base_url: baseURLFromUserConfig } = userData; + const userData = getUserDataJson(); + const { api_key: apiKeyFromUserConfig, base_url: baseURLFromUserConfig } = + userData; - const baseURLParsed = baseUrl || getEnvVariable("COMPOSIO_BASE_URL") || baseURLFromUserConfig || DEFAULT_BASE_URL; - const apiKeyParsed = apiKey || getEnvVariable("COMPOSIO_API_KEY") || apiKeyFromUserConfig || ''; + const baseURLParsed = + baseUrl || + getEnvVariable("COMPOSIO_BASE_URL") || + baseURLFromUserConfig || + DEFAULT_BASE_URL; + const apiKeyParsed = + apiKey || getEnvVariable("COMPOSIO_API_KEY") || apiKeyFromUserConfig || ""; - return { baseURL: baseURLParsed, apiKey: apiKeyParsed }; + return { baseURL: baseURLParsed, apiKey: apiKeyParsed }; } // Get the API client export function getOpenAPIClient(baseUrl?: string, apiKey?: string) { - const { baseURL, apiKey: apiKeyParsed } = getSDKConfig(baseUrl, apiKey); - - axiosClient.setConfig({ - baseURL, - headers: { - 'X-API-KEY': apiKeyParsed, - 'X-SOURCE': 'js_sdk', - 'X-RUNTIME': 'js_sdk' - }, - throwOnError: true - }); + const { baseURL, apiKey: apiKeyParsed } = getSDKConfig(baseUrl, apiKey); - setAxiosClientConfig(axiosClient.instance); - return apiClient; -} + axiosClient.setConfig({ + baseURL, + headers: { + "X-API-KEY": apiKeyParsed, + "X-SOURCE": "js_sdk", + "X-RUNTIME": "js_sdk", + }, + throwOnError: true, + }); + setAxiosClientConfig(axiosClient.instance); + return apiClient; +} diff --git a/js/src/sdk/utils/constants.ts b/js/src/sdk/utils/constants.ts index 5e7658d5e97..653d2d28dd7 100644 --- a/js/src/sdk/utils/constants.ts +++ b/js/src/sdk/utils/constants.ts @@ -3,6 +3,7 @@ export const LOCAL_CACHE_DIRECTORY_NAME = ".composio"; export const USER_DATA_FILE_NAME = "user_data.json"; export const DEFAULT_BASE_URL = "https://backend.composio.dev"; -export const TELEMETRY_URL = "https://app.composio.dev" +export const TELEMETRY_URL = "https://app.composio.dev"; -export const IS_DEVELOPMENT_OR_CI = (process.env.DEVELOPMENT || process.env.CI) || false; +export const IS_DEVELOPMENT_OR_CI = + process.env.DEVELOPMENT || process.env.CI || false; diff --git a/js/src/sdk/utils/error.ts b/js/src/sdk/utils/error.ts index adbc81d9747..5bb37f1bdfc 100644 --- a/js/src/sdk/utils/error.ts +++ b/js/src/sdk/utils/error.ts @@ -1,160 +1,197 @@ -import { SDK_ERROR_CODES, BASE_ERROR_CODE_INFO, BE_STATUS_CODE_TO_SDK_ERROR_CODES } from "./errors/src/constants"; +import { + SDK_ERROR_CODES, + BASE_ERROR_CODE_INFO, + BE_STATUS_CODE_TO_SDK_ERROR_CODES, +} from "./errors/src/constants"; import { ComposioError } from "./errors/src/composioError"; -import { generateMetadataFromAxiosError, getAPIErrorDetails } from "./errors/src/formatter"; +import { + generateMetadataFromAxiosError, + getAPIErrorDetails, +} from "./errors/src/formatter"; import { AxiosError } from "axios"; import { ZodError } from "zod"; export class CEG { - - static handleAllError(error: unknown, shouldThrow: boolean = false) { - if(error instanceof ComposioError) { - if(shouldThrow) { - throw error; - } - return error; - } - - if (!(error instanceof Error)) { - const error = new Error("Passed error is not an instance of Error"); - if(shouldThrow) { - throw error; - } - return error; - } - - if(error instanceof ZodError) { - const zodError = this.returnZodError(error); - if(shouldThrow) { - throw zodError; - } - return zodError; - } - - const isAxiosError = (error as AxiosError).isAxiosError; - - if (!isAxiosError) { - const customError = this.getCustomError(SDK_ERROR_CODES.COMMON.UNKNOWN, { - message: error.message, - description: "", - possibleFix: "Please check error message and stack trace", - originalError: error, - metadata: {} - }); - if(shouldThrow) { - throw customError; - } - return customError; - } else { - const isResponseNotPresent = !('response' in error); - if(isResponseNotPresent) { - const nonResponseError = this.handleNonResponseAxiosError(error as AxiosError); - if(shouldThrow) { - throw nonResponseError; - } - return nonResponseError; - } - const apiError = this.throwAPIError(error as AxiosError); - if(shouldThrow) { - throw apiError; - } - return apiError; - } + static handleAllError(error: unknown, shouldThrow: boolean = false) { + if (error instanceof ComposioError) { + if (shouldThrow) { + throw error; + } + return error; } - private static handleNonResponseAxiosError(error: AxiosError) { - const fullUrl = (error.config?.baseURL || "") + (error.config?.url || ""); - const metadata = generateMetadataFromAxiosError(error); - - if (error.code === "ECONNREFUSED") { - throw new ComposioError( - SDK_ERROR_CODES.COMMON.BASE_URL_NOT_REACHABLE, - `ECONNREFUSED for ${fullUrl}`, - "", - "Make sure:\n1. The base URL is correct and is accessible\n2. Your network connection is stable\n3. There are no firewall rules blocking the connection", - metadata, - error - ); - } - - if ( error.code === "ETIMEDOUT") { - throw new ComposioError( - SDK_ERROR_CODES.COMMON.REQUEST_TIMEOUT, - `ECONNABORTED for ${fullUrl}`, - `Request to ${fullUrl} timed out after the configured timeout period. This could be due to slow network conditions, server performance issues, or the request being too large. Error code: ECONNABORTED`, - "Try:\n1. Checking your network speed and stability\n2. Increasing the request timeout setting if needed\n3. Breaking up large requests into smaller chunks\n4. Retrying the request when network conditions improve\n5. Contact tech@composio.dev if the issue persists", - metadata, - error - ); - } + if (!(error instanceof Error)) { + const error = new Error("Passed error is not an instance of Error"); + if (shouldThrow) { + throw error; + } + return error; + } - if(error.code === "ECONNABORTED") { - throw new ComposioError( - SDK_ERROR_CODES.COMMON.REQUEST_ABORTED, - error.message, - "The request was aborted due to a timeout or other network-related issues. This could be due to network instability, server issues, or the request being too large. Error code: ECONNABORTED", - "Try:\n1. Checking your network speed and stability\n2. Increasing the request timeout setting if needed\n3. Breaking up large requests into smaller chunks\n4. Retrying the request when network conditions improve\n5. Contact tech@composio.dev if the issue persists", - metadata, - error - ); - } + if (error instanceof ZodError) { + const zodError = this.returnZodError(error); + if (shouldThrow) { + throw zodError; + } + return zodError; + } - throw new ComposioError( - SDK_ERROR_CODES.COMMON.UNKNOWN, - error.message, - "", - "Please contact tech@composio.dev with the error details.", - metadata, - error + const isAxiosError = (error as AxiosError).isAxiosError; + + if (!isAxiosError) { + const customError = this.getCustomError(SDK_ERROR_CODES.COMMON.UNKNOWN, { + message: error.message, + description: "", + possibleFix: "Please check error message and stack trace", + originalError: error, + metadata: {}, + }); + if (shouldThrow) { + throw customError; + } + return customError; + } else { + const isResponseNotPresent = !("response" in error); + if (isResponseNotPresent) { + const nonResponseError = this.handleNonResponseAxiosError( + error as AxiosError ); + if (shouldThrow) { + throw nonResponseError; + } + return nonResponseError; + } + const apiError = this.throwAPIError(error as AxiosError); + if (shouldThrow) { + throw apiError; + } + return apiError; + } + } + + private static handleNonResponseAxiosError(error: AxiosError) { + const fullUrl = (error.config?.baseURL || "") + (error.config?.url || ""); + const metadata = generateMetadataFromAxiosError(error); + + if (error.code === "ECONNREFUSED") { + throw new ComposioError( + SDK_ERROR_CODES.COMMON.BASE_URL_NOT_REACHABLE, + `ECONNREFUSED for ${fullUrl}`, + "", + "Make sure:\n1. The base URL is correct and is accessible\n2. Your network connection is stable\n3. There are no firewall rules blocking the connection", + metadata, + error + ); } - static throwAPIError(error: AxiosError) { - const statusCode = error?.response?.status || null; - const errorCode = statusCode ? BE_STATUS_CODE_TO_SDK_ERROR_CODES[statusCode] || SDK_ERROR_CODES.BACKEND.UNKNOWN : SDK_ERROR_CODES.BACKEND.UNKNOWN; - const predefinedError = BASE_ERROR_CODE_INFO[errorCode]; - - const errorDetails = getAPIErrorDetails(errorCode, error, predefinedError); - - const metadata = generateMetadataFromAxiosError(error); - throw new ComposioError(errorCode, errorDetails.message, errorDetails.description, errorDetails.possibleFix, metadata, error) + if (error.code === "ETIMEDOUT") { + throw new ComposioError( + SDK_ERROR_CODES.COMMON.REQUEST_TIMEOUT, + `ECONNABORTED for ${fullUrl}`, + `Request to ${fullUrl} timed out after the configured timeout period. This could be due to slow network conditions, server performance issues, or the request being too large. Error code: ECONNABORTED`, + "Try:\n1. Checking your network speed and stability\n2. Increasing the request timeout setting if needed\n3. Breaking up large requests into smaller chunks\n4. Retrying the request when network conditions improve\n5. Contact tech@composio.dev if the issue persists", + metadata, + error + ); } - static returnZodError(error: ZodError) { - const errorCode = SDK_ERROR_CODES.COMMON.INVALID_PARAMS_PASSED; - const errorMessage = error.message; - const errorDescription = "The parameters passed are invalid"; - const possibleFix = "Please check the metadata.issues for more details"; - const metadata = { - issues: error.issues - }; - - return new ComposioError(errorCode, errorMessage, errorDescription, possibleFix, metadata, error); + if (error.code === "ECONNABORTED") { + throw new ComposioError( + SDK_ERROR_CODES.COMMON.REQUEST_ABORTED, + error.message, + "The request was aborted due to a timeout or other network-related issues. This could be due to network instability, server issues, or the request being too large. Error code: ECONNABORTED", + "Try:\n1. Checking your network speed and stability\n2. Increasing the request timeout setting if needed\n3. Breaking up large requests into smaller chunks\n4. Retrying the request when network conditions improve\n5. Contact tech@composio.dev if the issue persists", + metadata, + error + ); } - static getCustomError(messageCode: string, { - message, - type, - subtype, - description, - possibleFix, - originalError, - metadata + throw new ComposioError( + SDK_ERROR_CODES.COMMON.UNKNOWN, + error.message, + "", + "Please contact tech@composio.dev with the error details.", + metadata, + error + ); + } + + static throwAPIError(error: AxiosError) { + const statusCode = error?.response?.status || null; + const errorCode = statusCode + ? BE_STATUS_CODE_TO_SDK_ERROR_CODES[statusCode] || + SDK_ERROR_CODES.BACKEND.UNKNOWN + : SDK_ERROR_CODES.BACKEND.UNKNOWN; + const predefinedError = BASE_ERROR_CODE_INFO[errorCode]; + + const errorDetails = getAPIErrorDetails(errorCode, error, predefinedError); + + const metadata = generateMetadataFromAxiosError(error); + throw new ComposioError( + errorCode, + errorDetails.message, + errorDetails.description, + errorDetails.possibleFix, + metadata, + error + ); + } + + static returnZodError(error: ZodError) { + const errorCode = SDK_ERROR_CODES.COMMON.INVALID_PARAMS_PASSED; + const errorMessage = error.message; + const errorDescription = "The parameters passed are invalid"; + const possibleFix = "Please check the metadata.issues for more details"; + const metadata = { + issues: error.issues, + }; + + return new ComposioError( + errorCode, + errorMessage, + errorDescription, + possibleFix, + metadata, + error + ); + } + + static getCustomError( + messageCode: string, + { + message, + type, + subtype, + description, + possibleFix, + originalError, + metadata, }: { - type?: string; - subtype?: string; - message?: string; - description?: string; - possibleFix?: string; - originalError?: unknown; - metadata?: Record; - }): never { - const finalErrorCode = !!messageCode ? messageCode : `${type}::${subtype}`; - const errorDetails = BASE_ERROR_CODE_INFO[finalErrorCode] || BASE_ERROR_CODE_INFO.UNKNOWN; - - const finalMessage = message || errorDetails.message || ""; - const finalDescription = description || errorDetails.description || undefined; - const finalPossibleFix = possibleFix || errorDetails.possibleFix || ""; - - throw new ComposioError(messageCode, finalMessage, finalDescription, finalPossibleFix, metadata, originalError); + type?: string; + subtype?: string; + message?: string; + description?: string; + possibleFix?: string; + originalError?: unknown; + metadata?: Record; } + ): never { + const finalErrorCode = !!messageCode ? messageCode : `${type}::${subtype}`; + const errorDetails = + BASE_ERROR_CODE_INFO[finalErrorCode] || BASE_ERROR_CODE_INFO.UNKNOWN; + + const finalMessage = message || errorDetails.message || ""; + const finalDescription = + description || errorDetails.description || undefined; + const finalPossibleFix = possibleFix || errorDetails.possibleFix || ""; + + throw new ComposioError( + messageCode, + finalMessage, + finalDescription, + finalPossibleFix, + metadata, + originalError + ); + } } diff --git a/js/src/sdk/utils/errors/index.ts b/js/src/sdk/utils/errors/index.ts index 012e4a64bf7..395f887ba12 100644 --- a/js/src/sdk/utils/errors/index.ts +++ b/js/src/sdk/utils/errors/index.ts @@ -4,72 +4,73 @@ import { sendBrowserReq, sendProcessReq } from "../../../utils/external"; import { getEnvVariable } from "../../../utils/shared"; type ErrorPayload = { - error_id: string, - error_code: string, - original_error: string, - description: string, - metadata: Record, - message: string, - possible_fix: string, - current_stack: string[], -} + error_id: string; + error_code: string; + original_error: string; + description: string; + metadata: Record; + message: string; + possible_fix: string; + current_stack: string[]; +}; export async function logError(payload: ErrorPayload) { - const isTelementryDisabled = getEnvVariable("TELEMETRY_DISABLED", "false") === "true"; - if(isTelementryDisabled) { - return; - } - try { - const isBrowser = typeof window !== 'undefined'; - const reportingPayload = await generateReportingPayload(payload); - const reqPayload = { - data: reportingPayload, - url: `${TELEMETRY_URL}/api/sdk_metrics/error`, - method: "POST", - headers: { - "Content-Type": "application/json" - } - } - - if (isBrowser) { - await sendBrowserReq(reqPayload); - } else { - await sendProcessReq(reqPayload); - } - } catch (error) { - console.error("Error sending error to telemetry", error); - // DO NOTHING + const isTelementryDisabled = + getEnvVariable("TELEMETRY_DISABLED", "false") === "true"; + if (isTelementryDisabled) { + return; + } + try { + const isBrowser = typeof window !== "undefined"; + const reportingPayload = await generateReportingPayload(payload); + const reqPayload = { + data: reportingPayload, + url: `${TELEMETRY_URL}/api/sdk_metrics/error`, + method: "POST", + headers: { + "Content-Type": "application/json", + }, + }; + + if (isBrowser) { + await sendBrowserReq(reqPayload); + } else { + await sendProcessReq(reqPayload); } + } catch (error) { + console.error("Error sending error to telemetry", error); + // DO NOTHING + } } async function generateReportingPayload(payload: ErrorPayload) { + const { apiKey, baseURL, composioVersion, frameworkRuntime, source } = + ComposioSDKContext; + const { + error_id, + error_code, + description, + message, + possible_fix, + original_error, + current_stack, + } = payload; - const { apiKey, baseURL, composioVersion, frameworkRuntime, source } = ComposioSDKContext - const { - error_id, - error_code, - description, - message, - possible_fix, - original_error, - current_stack - } = payload; - - return { - error_id, - error_code, - description, - error_message: message, - possible_fix, - original_error, - current_stack, - sdk_meta: { - platform: process.platform, - version: composioVersion, - baseURL, - apiKey, - frameworkRuntime, - source - } - }; + return { + error_id, + error_code, + description, + error_message: message, + possible_fix, + original_error, + current_stack, + sdk_meta: { + platform: process.platform, + version: composioVersion, + baseURL, + apiKey, + frameworkRuntime, + source, + }, + }; } diff --git a/js/src/sdk/utils/errors/src/composioError.ts b/js/src/sdk/utils/errors/src/composioError.ts index 895dfa193a4..75edcdc1f2b 100644 --- a/js/src/sdk/utils/errors/src/composioError.ts +++ b/js/src/sdk/utils/errors/src/composioError.ts @@ -1,115 +1,117 @@ -import { v4 as uuidv4 } from 'uuid'; -import { getLogLevel } from '../../../../utils/logger'; -import { logError } from '..'; +import { v4 as uuidv4 } from "uuid"; +import { getLogLevel } from "../../../../utils/logger"; +import { logError } from ".."; /** * Custom error class for Composio that provides rich error details, tracking, and improved debugging */ export class ComposioError extends Error { - // time at which the error occurred - public readonly timestamp: string; - - // unique identifier for the error - private readonly errorId: string; - - // error code - private readonly errCode: string; - - // additional metadata about the error - private readonly metadata?: Record = {}; - - // description of the error - private readonly description?: string; - - // possible fix for the error - private readonly possibleFix?: string; - - // original error object - private readonly _originalError?: any; - - constructor( - errCode: string, - message: string, - description?: string, - possibleFix?: string, - metadata?: Record, - originalError?: any, - ) { - // Ensure message is never empty - super(message || 'An unknown error occurred'); - - // Ensure proper prototype chain for instanceof checks - Object.setPrototypeOf(this, new.target.prototype); - - this.name = 'ComposioError'; - this.errCode = errCode; - this.description = description; - this.possibleFix = possibleFix; - this.timestamp = new Date().toISOString(); - this.metadata = metadata; - this.errorId = uuidv4(); - - let originalErrorString: string = ''; - - // Only print original error if COMPOSIO_LOGGING_LEVEL is debug - if (originalError) { - try { - originalErrorString = typeof originalError === 'object' - ? JSON.parse(JSON.stringify(originalError)) - : originalError; - } catch (e) { - originalErrorString = String(originalError); - } - - if (getLogLevel() === 'debug') { - this._originalError = originalErrorString; - } - } - - logError({ - error_id: this.errorId, - error_code: this.errCode, - original_error: originalErrorString, - description: this.description || '', - metadata: this.metadata || {}, - message: this.message, - possible_fix: this.possibleFix || '', - current_stack: this.stack?.split('\n') || [] - }); - - - // Capture stack trace, excluding constructor call - Error.captureStackTrace(this, this.constructor); - } - - get originalError(): any { - return this._originalError; + // time at which the error occurred + public readonly timestamp: string; + + // unique identifier for the error + private readonly errorId: string; + + // error code + private readonly errCode: string; + + // additional metadata about the error + private readonly metadata?: Record = {}; + + // description of the error + private readonly description?: string; + + // possible fix for the error + private readonly possibleFix?: string; + + // original error object + private readonly _originalError?: any; + + constructor( + errCode: string, + message: string, + description?: string, + possibleFix?: string, + metadata?: Record, + originalError?: any + ) { + // Ensure message is never empty + super(message || "An unknown error occurred"); + + // Ensure proper prototype chain for instanceof checks + Object.setPrototypeOf(this, new.target.prototype); + + this.name = "ComposioError"; + this.errCode = errCode; + this.description = description; + this.possibleFix = possibleFix; + this.timestamp = new Date().toISOString(); + this.metadata = metadata; + this.errorId = uuidv4(); + + let originalErrorString: string = ""; + + // Only print original error if COMPOSIO_LOGGING_LEVEL is debug + if (originalError) { + try { + originalErrorString = + typeof originalError === "object" + ? JSON.parse(JSON.stringify(originalError)) + : originalError; + } catch (e) { + originalErrorString = String(originalError); + } + + if (getLogLevel() === "debug") { + this._originalError = originalErrorString; + } } - - /** - * Returns a complete object representation for logging/serialization - * Includes all error details and metadata - */ - toJSON(): Record { - const errorObj = { - name: this.name, - errorId: this.errorId, - code: this.errCode, - message: this.message, - description: this.description, - possibleFix: this.possibleFix, - timestamp: this.timestamp, - stack: this.stack?.split('\n'), - originalStack: this.originalError?.stack?.split('\n'), - }; - - // Remove undefined/null properties - return Object.entries(errorObj).reduce((acc, [key, value]) => { - if (value !== undefined && value !== null) { - acc[key] = value; - } - return acc; - }, {} as Record); - } -} \ No newline at end of file + logError({ + error_id: this.errorId, + error_code: this.errCode, + original_error: originalErrorString, + description: this.description || "", + metadata: this.metadata || {}, + message: this.message, + possible_fix: this.possibleFix || "", + current_stack: this.stack?.split("\n") || [], + }); + + // Capture stack trace, excluding constructor call + Error.captureStackTrace(this, this.constructor); + } + + get originalError(): any { + return this._originalError; + } + + /** + * Returns a complete object representation for logging/serialization + * Includes all error details and metadata + */ + toJSON(): Record { + const errorObj = { + name: this.name, + errorId: this.errorId, + code: this.errCode, + message: this.message, + description: this.description, + possibleFix: this.possibleFix, + timestamp: this.timestamp, + stack: this.stack?.split("\n"), + originalStack: this.originalError?.stack?.split("\n"), + }; + + // Remove undefined/null properties + return Object.entries(errorObj).reduce( + (acc, [key, value]) => { + if (value !== undefined && value !== null) { + acc[key] = value; + } + return acc; + }, + {} as Record + ); + } +} diff --git a/js/src/sdk/utils/errors/src/constants.ts b/js/src/sdk/utils/errors/src/constants.ts index 7a4b6541ff2..c4b8f1db799 100644 --- a/js/src/sdk/utils/errors/src/constants.ts +++ b/js/src/sdk/utils/errors/src/constants.ts @@ -1,100 +1,104 @@ - -export const SDK_ERROR_CODES = { - BACKEND: { - NOT_FOUND: "BACKEND::NOT_FOUND", - RATE_LIMIT: "BACKEND::RATE_LIMIT", - BAD_REQUEST: "BACKEND::BAD_REQUEST", - UNAUTHORIZED: "BACKEND::UNAUTHORIZED", - SERVER_ERROR: "BACKEND::SERVER_ERROR", - SERVER_UNAVAILABLE: "BACKEND::SERVER_UNAVAILABLE", - UNKNOWN: "BACKEND::UNKNOWN", - }, - COMMON: { - API_KEY_UNAVAILABLE: "COMMON::API_KEY_INVALID", - BASE_URL_NOT_REACHABLE: "COMMON::BASE_URL_NOT_REACHABLE", - UNKNOWN: "COMMON::ERROR_CODE_NOT_DEFINED", - SERVER_UNAVAILABLE: "COMMON::SERVER_UNAVAILABLE", - REQUEST_TIMEOUT: "COMMON::REQUEST_TIMEOUT", - REQUEST_ABORTED: "COMMON::REQUEST_ABORTED", - INVALID_PARAMS_PASSED: "COMMON::INVALID_PARAMS_PASSED", - }, - SDK:{ - NO_CONNECTED_ACCOUNT_FOUND: "SDK::NO_CONNECTED_ACCOUNT_FOUND", - } -} +export const SDK_ERROR_CODES = { + BACKEND: { + NOT_FOUND: "BACKEND::NOT_FOUND", + RATE_LIMIT: "BACKEND::RATE_LIMIT", + BAD_REQUEST: "BACKEND::BAD_REQUEST", + UNAUTHORIZED: "BACKEND::UNAUTHORIZED", + SERVER_ERROR: "BACKEND::SERVER_ERROR", + SERVER_UNAVAILABLE: "BACKEND::SERVER_UNAVAILABLE", + UNKNOWN: "BACKEND::UNKNOWN", + }, + COMMON: { + API_KEY_UNAVAILABLE: "COMMON::API_KEY_INVALID", + BASE_URL_NOT_REACHABLE: "COMMON::BASE_URL_NOT_REACHABLE", + UNKNOWN: "COMMON::ERROR_CODE_NOT_DEFINED", + SERVER_UNAVAILABLE: "COMMON::SERVER_UNAVAILABLE", + REQUEST_TIMEOUT: "COMMON::REQUEST_TIMEOUT", + REQUEST_ABORTED: "COMMON::REQUEST_ABORTED", + INVALID_PARAMS_PASSED: "COMMON::INVALID_PARAMS_PASSED", + }, + SDK: { + NO_CONNECTED_ACCOUNT_FOUND: "SDK::NO_CONNECTED_ACCOUNT_FOUND", + }, +}; export const BASE_ERROR_CODE_INFO = { - [SDK_ERROR_CODES.BACKEND.NOT_FOUND]: { - message: "🔍 API not found", - description: "The requested resource is missing", - possibleFix: "Verify the URL or resource identifier." - }, - [SDK_ERROR_CODES.BACKEND.BAD_REQUEST]: { - message: "🚫 Bad Request. The request was malformed or incorrect", - description: null, - possibleFix: "Please check your request format and parameters." - }, - [SDK_ERROR_CODES.BACKEND.UNAUTHORIZED]: { - message: "🔑 Access Denied", - description: "You do not have the necessary credentials.", - possibleFix: "Ensure your API key is correct and has the required permissions." - }, - [SDK_ERROR_CODES.COMMON.REQUEST_TIMEOUT]: { - message: "🕒 Request Timeout", - description: "The request timed out while waiting for a response.", - possibleFix: "Please try again later. If the issue persists, contact support." - }, - [SDK_ERROR_CODES.BACKEND.SERVER_ERROR]: { - message: "💥 Oops! Internal server error", - description: "Your request could not be processed due to an internal server error.", - possibleFix: "Please try again later. If the issue persists, contact support." - }, - [SDK_ERROR_CODES.BACKEND.RATE_LIMIT]: { - message: "⏱️ API Rate Limit Exceeded", - description: "You have exceeded the rate limit for requests.", - possibleFix: "Please wait a bit before trying your request again." - }, - [SDK_ERROR_CODES.COMMON.API_KEY_UNAVAILABLE]: { - message: "🔑 API Key Missing or Invalid", - description: "The API key provided is missing or incorrect.", - possibleFix: "Ensure that your API key is passed to client or set in COMPOSIO_API_KEY environment variable." - }, - [SDK_ERROR_CODES.BACKEND.SERVER_UNAVAILABLE]: { - message: "🚫 Server Unavailable", - description: "The server is currently unable to handle the request.", - possibleFix: "Please try again later. If the issue persists, contact support." - }, - [SDK_ERROR_CODES.COMMON.BASE_URL_NOT_REACHABLE]: { - message: "🔗 Base URL is not valid", - description: "The base URL provided is not valid.", - possibleFix: "Ensure that the base URL is correct and accessible." - }, - [SDK_ERROR_CODES.COMMON.INVALID_PARAMS_PASSED]: { - message: "🕒 Invalid parameters passed", - description: "The parameters passed are invalid", - possibleFix: "Please check the metadata.issues for more details" - }, - UNKNOWN: { - message: null, - description: null, - possibleFix: "Contact our support team with the error details for further assistance." - }, - [SDK_ERROR_CODES.BACKEND.UNKNOWN]: { - message: null, - description: null, - possibleFix: "Contact our support team with the error details for further assistance." - } -} - + [SDK_ERROR_CODES.BACKEND.NOT_FOUND]: { + message: "🔍 API not found", + description: "The requested resource is missing", + possibleFix: "Verify the URL or resource identifier.", + }, + [SDK_ERROR_CODES.BACKEND.BAD_REQUEST]: { + message: "🚫 Bad Request. The request was malformed or incorrect", + description: null, + possibleFix: "Please check your request format and parameters.", + }, + [SDK_ERROR_CODES.BACKEND.UNAUTHORIZED]: { + message: "🔑 Access Denied", + description: "You do not have the necessary credentials.", + possibleFix: + "Ensure your API key is correct and has the required permissions.", + }, + [SDK_ERROR_CODES.COMMON.REQUEST_TIMEOUT]: { + message: "🕒 Request Timeout", + description: "The request timed out while waiting for a response.", + possibleFix: + "Please try again later. If the issue persists, contact support.", + }, + [SDK_ERROR_CODES.BACKEND.SERVER_ERROR]: { + message: "💥 Oops! Internal server error", + description: + "Your request could not be processed due to an internal server error.", + possibleFix: + "Please try again later. If the issue persists, contact support.", + }, + [SDK_ERROR_CODES.BACKEND.RATE_LIMIT]: { + message: "⏱️ API Rate Limit Exceeded", + description: "You have exceeded the rate limit for requests.", + possibleFix: "Please wait a bit before trying your request again.", + }, + [SDK_ERROR_CODES.COMMON.API_KEY_UNAVAILABLE]: { + message: "🔑 API Key Missing or Invalid", + description: "The API key provided is missing or incorrect.", + possibleFix: + "Ensure that your API key is passed to client or set in COMPOSIO_API_KEY environment variable.", + }, + [SDK_ERROR_CODES.BACKEND.SERVER_UNAVAILABLE]: { + message: "🚫 Server Unavailable", + description: "The server is currently unable to handle the request.", + possibleFix: + "Please try again later. If the issue persists, contact support.", + }, + [SDK_ERROR_CODES.COMMON.BASE_URL_NOT_REACHABLE]: { + message: "🔗 Base URL is not valid", + description: "The base URL provided is not valid.", + possibleFix: "Ensure that the base URL is correct and accessible.", + }, + [SDK_ERROR_CODES.COMMON.INVALID_PARAMS_PASSED]: { + message: "🕒 Invalid parameters passed", + description: "The parameters passed are invalid", + possibleFix: "Please check the metadata.issues for more details", + }, + UNKNOWN: { + message: null, + description: null, + possibleFix: + "Contact our support team with the error details for further assistance.", + }, + [SDK_ERROR_CODES.BACKEND.UNKNOWN]: { + message: null, + description: null, + possibleFix: + "Contact our support team with the error details for further assistance.", + }, +}; export const BE_STATUS_CODE_TO_SDK_ERROR_CODES = { - 400: SDK_ERROR_CODES.BACKEND.BAD_REQUEST, - 401: SDK_ERROR_CODES.BACKEND.UNAUTHORIZED, - 404: SDK_ERROR_CODES.BACKEND.NOT_FOUND, - 408: SDK_ERROR_CODES.COMMON.REQUEST_TIMEOUT, - 429: SDK_ERROR_CODES.BACKEND.RATE_LIMIT, - 500: SDK_ERROR_CODES.BACKEND.SERVER_ERROR, - 502: SDK_ERROR_CODES.BACKEND.SERVER_UNAVAILABLE, - + 400: SDK_ERROR_CODES.BACKEND.BAD_REQUEST, + 401: SDK_ERROR_CODES.BACKEND.UNAUTHORIZED, + 404: SDK_ERROR_CODES.BACKEND.NOT_FOUND, + 408: SDK_ERROR_CODES.COMMON.REQUEST_TIMEOUT, + 429: SDK_ERROR_CODES.BACKEND.RATE_LIMIT, + 500: SDK_ERROR_CODES.BACKEND.SERVER_ERROR, + 502: SDK_ERROR_CODES.BACKEND.SERVER_UNAVAILABLE, } as Record; - diff --git a/js/src/sdk/utils/errors/src/formatter.ts b/js/src/sdk/utils/errors/src/formatter.ts index f21d0c3021b..da3ff578cf3 100644 --- a/js/src/sdk/utils/errors/src/formatter.ts +++ b/js/src/sdk/utils/errors/src/formatter.ts @@ -1,86 +1,104 @@ import { ComposioError } from "./composioError"; -import { SDK_ERROR_CODES, BASE_ERROR_CODE_INFO, BE_STATUS_CODE_TO_SDK_ERROR_CODES } from "./constants"; +import { + SDK_ERROR_CODES, + BASE_ERROR_CODE_INFO, + BE_STATUS_CODE_TO_SDK_ERROR_CODES, +} from "./constants"; import { AxiosError } from "axios"; interface ErrorResponse { - errorKey: string; - message: string; - description: string; - possibleFix: string; - metadata?: Record; + errorKey: string; + message: string; + description: string; + possibleFix: string; + metadata?: Record; } interface ErrorDetails { - message: string; - description: string; - possibleFix: string; - metadata?: Record; + message: string; + description: string; + possibleFix: string; + metadata?: Record; } +export const getAPIErrorDetails = ( + errorKey: string, + axiosError: any, + predefinedError: any +): ErrorDetails => { + const defaultErrorDetails = { + message: axiosError.message, + description: + axiosError.response?.data?.message || + axiosError.response?.data?.error || + axiosError.message, + possibleFix: + "Please check the network connection, request parameters, and ensure the API endpoint is correct.", + }; -export const getAPIErrorDetails = (errorKey: string, axiosError: any, predefinedError: any): ErrorDetails => { - const defaultErrorDetails = { - message: axiosError.message, - description: axiosError.response?.data?.message || axiosError.response?.data?.error || axiosError.message, - possibleFix: "Please check the network connection, request parameters, and ensure the API endpoint is correct." - }; + const metadata = generateMetadataFromAxiosError(axiosError); + switch (errorKey) { + case SDK_ERROR_CODES.BACKEND.NOT_FOUND: + case SDK_ERROR_CODES.BACKEND.UNAUTHORIZED: + case SDK_ERROR_CODES.BACKEND.SERVER_ERROR: + case SDK_ERROR_CODES.BACKEND.SERVER_UNAVAILABLE: + case SDK_ERROR_CODES.BACKEND.RATE_LIMIT: + return { + message: `${predefinedError.message || axiosError.message} for ${axiosError.config.baseURL + axiosError.config.url}`, + description: + axiosError.response?.data?.message || predefinedError.description, + possibleFix: + predefinedError.possibleFix || defaultErrorDetails.possibleFix, + metadata, + }; - const metadata = generateMetadataFromAxiosError(axiosError); - switch (errorKey) { - case SDK_ERROR_CODES.BACKEND.NOT_FOUND: - case SDK_ERROR_CODES.BACKEND.UNAUTHORIZED: - case SDK_ERROR_CODES.BACKEND.SERVER_ERROR: - case SDK_ERROR_CODES.BACKEND.SERVER_UNAVAILABLE: - case SDK_ERROR_CODES.BACKEND.RATE_LIMIT: - return { - message: `${predefinedError.message || axiosError.message} for ${axiosError.config.baseURL + axiosError.config.url}`, - description: axiosError.response?.data?.message || predefinedError.description, - possibleFix: predefinedError.possibleFix || defaultErrorDetails.possibleFix, - metadata - }; + case SDK_ERROR_CODES.BACKEND.BAD_REQUEST: + const validationErrors = axiosError.response?.data?.errors; + const formattedErrors = Array.isArray(validationErrors) + ? validationErrors.map((err) => JSON.stringify(err)).join(", ") + : JSON.stringify(validationErrors); - case SDK_ERROR_CODES.BACKEND.BAD_REQUEST: - const validationErrors = axiosError.response?.data?.errors; - const formattedErrors = Array.isArray(validationErrors) - ? validationErrors.map(err => JSON.stringify(err)).join(", ") - : JSON.stringify(validationErrors); + return { + message: `Validation Errors while making request to ${axiosError.config.baseURL + axiosError.config.url}`, + description: `Validation Errors: ${formattedErrors}`, + possibleFix: + "Please check the request parameters and ensure they are correct.", + metadata, + }; - return { - message: `Validation Errors while making request to ${axiosError.config.baseURL + axiosError.config.url}`, - description: `Validation Errors: ${formattedErrors}`, - possibleFix: "Please check the request parameters and ensure they are correct.", - metadata - }; + case SDK_ERROR_CODES.BACKEND.UNKNOWN: + case SDK_ERROR_CODES.COMMON.UNKNOWN: + return { + message: `${axiosError.message} for ${axiosError.config.baseURL + axiosError.config.url}`, + description: + axiosError.response?.data?.message || + axiosError.response?.data?.error || + axiosError.message, + possibleFix: "Please contact tech@composio.dev with the error details.", + metadata, + }; - case SDK_ERROR_CODES.BACKEND.UNKNOWN: - case SDK_ERROR_CODES.COMMON.UNKNOWN: - return { - message: `${axiosError.message} for ${axiosError.config.baseURL + axiosError.config.url}`, - description: axiosError.response?.data?.message || axiosError.response?.data?.error || axiosError.message, - possibleFix: "Please contact tech@composio.dev with the error details.", - metadata - }; - - default: - return { - message: `${predefinedError.message || axiosError.message} for ${axiosError.config.baseURL + axiosError.config.url}`, - description: axiosError.response?.data?.message || predefinedError.description, - possibleFix: predefinedError.possibleFix || defaultErrorDetails.possibleFix, - metadata - }; - } + default: + return { + message: `${predefinedError.message || axiosError.message} for ${axiosError.config.baseURL + axiosError.config.url}`, + description: + axiosError.response?.data?.message || predefinedError.description, + possibleFix: + predefinedError.possibleFix || defaultErrorDetails.possibleFix, + metadata, + }; + } }; - -export const generateMetadataFromAxiosError = (axiosError: any): Record => { - const requestId = axiosError.response?.headers["x-request-id"]; - return { - fullUrl: axiosError.config.baseURL + axiosError.config.url, - method: axiosError.config.method.toUpperCase(), - statusCode: axiosError.response?.status, - requestId: requestId ? `Request ID: ${requestId}` : undefined, - metadata: axiosError.metadata, - } -} - - +export const generateMetadataFromAxiosError = ( + axiosError: any +): Record => { + const requestId = axiosError.response?.headers["x-request-id"]; + return { + fullUrl: axiosError.config.baseURL + axiosError.config.url, + method: axiosError.config.method.toUpperCase(), + statusCode: axiosError.response?.status, + requestId: requestId ? `Request ID: ${requestId}` : undefined, + metadata: axiosError.metadata, + }; +}; diff --git a/js/src/sdk/utils/telemetry/events.ts b/js/src/sdk/utils/telemetry/events.ts index 9e3ae84fb4e..c8f1a426c51 100644 --- a/js/src/sdk/utils/telemetry/events.ts +++ b/js/src/sdk/utils/telemetry/events.ts @@ -1,5 +1,5 @@ export enum TELEMETRY_EVENTS { - SDK_INITIALIZED = "SDK_INITIALIZED", - SDK_METHOD_INVOKED = "SDK_METHOD_INVOKED", - CLI_INVOKED = "CLI_INVOKED" -} \ No newline at end of file + SDK_INITIALIZED = "SDK_INITIALIZED", + SDK_METHOD_INVOKED = "SDK_METHOD_INVOKED", + CLI_INVOKED = "CLI_INVOKED", +} diff --git a/js/src/sdk/utils/telemetry/index.ts b/js/src/sdk/utils/telemetry/index.ts index e3625ca7cd6..64ea610434f 100644 --- a/js/src/sdk/utils/telemetry/index.ts +++ b/js/src/sdk/utils/telemetry/index.ts @@ -5,60 +5,68 @@ import { BatchProcessor } from "../base/batchProcessor"; import { getEnvVariable } from "../../../utils/shared"; export class TELEMETRY_LOGGER { - private static batchProcessor = new BatchProcessor(1000, 100, async (data) => { - await TELEMETRY_LOGGER.sendTelemetry(data as Record[]); - }); - - private static createTelemetryWrapper(method: Function, className: string) { - return async (...args: unknown[]) => { - const payload = { - eventName: method.name, - data: { className, args }, - sdk_meta: ComposioSDKContext - }; - - TELEMETRY_LOGGER.batchProcessor.pushItem(payload); - return method(...args); - }; + private static batchProcessor = new BatchProcessor( + 1000, + 100, + async (data) => { + await TELEMETRY_LOGGER.sendTelemetry(data as Record[]); } + ); - private static async sendTelemetry(payload: Record[]) { - const isTelementryDisabled = getEnvVariable("TELEMETRY_DISABLED", "false") === "true"; - if(isTelementryDisabled) { - return; - } - const url = `${TELEMETRY_URL}/api/sdk_metrics/telemetry`; - const reqPayload = { - data: { events: payload }, - url, - method: "POST", - headers: { "Content-Type": "application/json" } - }; + private static createTelemetryWrapper(method: Function, className: string) { + return async (...args: unknown[]) => { + const payload = { + eventName: method.name, + data: { className, args }, + sdk_meta: ComposioSDKContext, + }; - const isBrowser = typeof window !== "undefined"; - if (isBrowser) { - await sendBrowserReq(reqPayload); - } else { - await sendProcessReq(reqPayload); - } - } + TELEMETRY_LOGGER.batchProcessor.pushItem(payload); + return method(...args); + }; + } - static wrapClassMethodsForTelemetry(classInstance: any, methods: string[]) { - methods.forEach((method) => { - classInstance[method] = TELEMETRY_LOGGER.createTelemetryWrapper(classInstance[method], classInstance.constructor.name); - }); + private static async sendTelemetry(payload: Record[]) { + const isTelementryDisabled = + getEnvVariable("TELEMETRY_DISABLED", "false") === "true"; + if (isTelementryDisabled) { + return; } + const url = `${TELEMETRY_URL}/api/sdk_metrics/telemetry`; + const reqPayload = { + data: { events: payload }, + url, + method: "POST", + headers: { "Content-Type": "application/json" }, + }; - static manualTelemetry(eventName: string, data: Record) { - const payload = { - eventName, - data, - sdk_meta: ComposioSDKContext - }; - TELEMETRY_LOGGER.batchProcessor.pushItem(payload); + const isBrowser = typeof window !== "undefined"; + if (isBrowser) { + await sendBrowserReq(reqPayload); + } else { + await sendProcessReq(reqPayload); } + } - static wrapFunctionForTelemetry(func: Function, className: string) { - return TELEMETRY_LOGGER.createTelemetryWrapper(func, className); - } + static wrapClassMethodsForTelemetry(classInstance: any, methods: string[]) { + methods.forEach((method) => { + classInstance[method] = TELEMETRY_LOGGER.createTelemetryWrapper( + classInstance[method], + classInstance.constructor.name + ); + }); + } + + static manualTelemetry(eventName: string, data: Record) { + const payload = { + eventName, + data, + sdk_meta: ComposioSDKContext, + }; + TELEMETRY_LOGGER.batchProcessor.pushItem(payload); + } + + static wrapFunctionForTelemetry(func: Function, className: string) { + return TELEMETRY_LOGGER.createTelemetryWrapper(func, className); + } } diff --git a/js/src/utils/external.ts b/js/src/utils/external.ts index 319010ac056..fcd1136a398 100644 --- a/js/src/utils/external.ts +++ b/js/src/utils/external.ts @@ -1,4 +1,3 @@ - import { spawn } from "child_process"; import { IS_DEVELOPMENT_OR_CI, TELEMETRY_URL } from "../sdk/utils/constants"; import { serializeValue } from "../sdk/utils/common"; @@ -6,22 +5,26 @@ import { serializeValue } from "../sdk/utils/common"; /** * Sends a reporting payload to the telemetry server using a child process. * This function is intended for use in Node.js environments. - * + * * @param {any} reportingPayload - The payload to be sent to the telemetry server. */ -export async function sendProcessReq(info:{ - url: string, - method: string, - headers: Record, - data: Record +export async function sendProcessReq(info: { + url: string; + method: string; + headers: Record; + data: Record; }) { - if(IS_DEVELOPMENT_OR_CI){ - console.log(`Hitting ${info.url}[${info.method}] with ${serializeValue(info.data)}`); - return true; - } - try { - // Spawn a child process to execute a Node.js script - const child = spawn('node', ['-e', ` + if (IS_DEVELOPMENT_OR_CI) { + console.log( + `Hitting ${info.url}[${info.method}] with ${serializeValue(info.data)}` + ); + return true; + } + try { + // Spawn a child process to execute a Node.js script + const child = spawn("node", [ + "-e", + ` const http = require('http'); const options = { hostname: '${info.url}', @@ -44,54 +47,57 @@ export async function sendProcessReq(info:{ req.write(JSON.stringify(info.data)); req.end(); - `]); - // Close the stdin stream - child.stdin.end(); - } catch (error) { - console.error("Error sending error to telemetry", error); - // DO NOTHING - } + `, + ]); + // Close the stdin stream + child.stdin.end(); + } catch (error) { + console.error("Error sending error to telemetry", error); + // DO NOTHING + } } /** * Sends a reporting payload to the telemetry server using XMLHttpRequest. * This function is intended for use in browser environments. - * + * * @param {any} reportingPayload - The payload to be sent to the telemetry server. */ -export async function sendBrowserReq(info:{ - url: string, - method: string, - headers: Record, - data: Record +export async function sendBrowserReq(info: { + url: string; + method: string; + headers: Record; + data: Record; }) { - if(IS_DEVELOPMENT_OR_CI){ - console.log(`Hitting ${info.url}[${info.method}] with ${serializeValue(info.data)}`); - return true; - } - try { - // Create a new XMLHttpRequest object - const xhr = new XMLHttpRequest(); - // Open a new POST request to the telemetry server - xhr.open(info.method, info.url, true); - // Set the request header to indicate JSON content - xhr.setRequestHeader('Content-Type', 'application/json'); - Object.entries(info.headers || {}).forEach(([key, value]) => { - xhr.setRequestHeader(key, value); - }); + if (IS_DEVELOPMENT_OR_CI) { + console.log( + `Hitting ${info.url}[${info.method}] with ${serializeValue(info.data)}` + ); + return true; + } + try { + // Create a new XMLHttpRequest object + const xhr = new XMLHttpRequest(); + // Open a new POST request to the telemetry server + xhr.open(info.method, info.url, true); + // Set the request header to indicate JSON content + xhr.setRequestHeader("Content-Type", "application/json"); + Object.entries(info.headers || {}).forEach(([key, value]) => { + xhr.setRequestHeader(key, value); + }); // Define the onload event handler - xhr.onload = function() { - // Log the response if the request was successful - if (xhr.status === 200) { - console.log(xhr.response); - } + xhr.onload = function () { + // Log the response if the request was successful + if (xhr.status === 200) { + console.log(xhr.response); + } }; // Send the reporting payload as a JSON string - xhr.send(JSON.stringify(info.data)); - } catch (error) { - console.error("Error sending error to telemetry", error); - // DO NOTHING - } -} \ No newline at end of file + xhr.send(JSON.stringify(info.data)); + } catch (error) { + console.error("Error sending error to telemetry", error); + // DO NOTHING + } +} diff --git a/js/src/utils/logger.ts b/js/src/utils/logger.ts index 79900f94e5c..1f644514efa 100644 --- a/js/src/utils/logger.ts +++ b/js/src/utils/logger.ts @@ -67,8 +67,8 @@ const logger = winston.createLogger({ transports: [ new winston.transports.Console({ handleExceptions: false, - handleRejections: false - }) + handleRejections: false, + }), ], exitOnError: false, // Prevent crashes on uncaught exceptions });