Compare commits

...

5 Commits

Author SHA1 Message Date
Vikhyath Mondreti
774e5d585c v0.5.15: add tools, revert subblock prop change 2025-12-01 13:52:12 -08:00
Vikhyath Mondreti
ede41af674 fix(selector): remove subblock state prop for subblock component (#2151) 2025-12-01 13:46:32 -08:00
Waleed
cb0c55c6f6 feat(tools): added rds, dynamodb, background color gradient (#2150)
* feat(tools): added rds tools/block

* feat(tools): added rds, dynamodb, background color gradient

* changed conditions for WHERE condition to be json conditions instead of raw string
2025-12-01 13:42:05 -08:00
Vikhyath Mondreti
54cc93743f v0.5.14: fix issue with teams, google selectors + cleanup code 2025-12-01 12:39:39 -08:00
Vikhyath Mondreti
d22b21c8d1 improvement(selectors): make serviceId sole source of truth (#2128)
* improvement(serviceId): make serviceId sole source of truth

* incorrect gmail service id

* fix teams selectors

* fix linkedin
2025-12-01 12:33:26 -08:00
107 changed files with 4201 additions and 729 deletions

File diff suppressed because one or more lines are too long

View File

@@ -16,6 +16,7 @@ import {
ConfluenceIcon,
DiscordIcon,
DocumentIcon,
DynamoDBIcon,
ElevenLabsIcon,
ExaAIIcon,
EyeIcon,
@@ -64,6 +65,7 @@ import {
PosthogIcon,
PylonIcon,
QdrantIcon,
RDSIcon,
RedditIcon,
ResendIcon,
S3Icon,
@@ -134,6 +136,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
s3: S3Icon,
resend: ResendIcon,
reddit: RedditIcon,
rds: RDSIcon,
qdrant: QdrantIcon,
pylon: PylonIcon,
posthog: PosthogIcon,
@@ -182,6 +185,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
file: DocumentIcon,
exa: ExaAIIcon,
elevenlabs: ElevenLabsIcon,
dynamodb: DynamoDBIcon,
discord: DiscordIcon,
confluence: ConfluenceIcon,
clay: ClayIcon,

View File

@@ -0,0 +1,193 @@
---
title: Amazon DynamoDB
description: Connect to Amazon DynamoDB
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="dynamodb"
color="linear-gradient(45deg, #2E27AD 0%, #527FFF 100%)"
/>
{/* MANUAL-CONTENT-START:intro */}
[Amazon DynamoDB](https://aws.amazon.com/dynamodb/) is a fully managed NoSQL database service offered by AWS that provides fast and predictable performance with seamless scalability. DynamoDB lets you store and retrieve any amount of data and serves any level of request traffic, without the need for you to manage hardware or infrastructure.
With DynamoDB, you can:
- **Get items**: Look up items in your tables using primary keys
- **Put items**: Add or replace items in your tables
- **Query items**: Retrieve multiple items using queries across indexes
- **Scan tables**: Read all or part of the data in a table
- **Update items**: Modify specific attributes of existing items
- **Delete items**: Remove records from your tables
In Sim, the DynamoDB integration enables your agents to securely access and manipulate DynamoDB tables using AWS credentials. Supported operations include:
- **Get**: Retrieve an item by its key
- **Put**: Insert or overwrite items
- **Query**: Run queries using key conditions and filters
- **Scan**: Read multiple items by scanning the table or index
- **Update**: Change specific attributes of one or more items
- **Delete**: Remove an item from a table
This integration empowers Sim agents to automate data management tasks within your DynamoDB tables programmatically, so you can build workflows that manage, modify, and retrieve scalable NoSQL data without manual effort or server management.
{/* MANUAL-CONTENT-END */}
## Usage Instructions
Integrate Amazon DynamoDB into workflows. Supports Get, Put, Query, Scan, Update, and Delete operations on DynamoDB tables.
## Tools
### `dynamodb_get`
Get an item from a DynamoDB table by primary key
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `region` | string | Yes | AWS region \(e.g., us-east-1\) |
| `accessKeyId` | string | Yes | AWS access key ID |
| `secretAccessKey` | string | Yes | AWS secret access key |
| `tableName` | string | Yes | DynamoDB table name |
| `key` | object | Yes | Primary key of the item to retrieve |
| `consistentRead` | boolean | No | Use strongly consistent read |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `message` | string | Operation status message |
| `item` | object | Retrieved item |
### `dynamodb_put`
Put an item into a DynamoDB table
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `region` | string | Yes | AWS region \(e.g., us-east-1\) |
| `accessKeyId` | string | Yes | AWS access key ID |
| `secretAccessKey` | string | Yes | AWS secret access key |
| `tableName` | string | Yes | DynamoDB table name |
| `item` | object | Yes | Item to put into the table |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `message` | string | Operation status message |
| `item` | object | Created item |
### `dynamodb_query`
Query items from a DynamoDB table using key conditions
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `region` | string | Yes | AWS region \(e.g., us-east-1\) |
| `accessKeyId` | string | Yes | AWS access key ID |
| `secretAccessKey` | string | Yes | AWS secret access key |
| `tableName` | string | Yes | DynamoDB table name |
| `keyConditionExpression` | string | Yes | Key condition expression \(e.g., "pk = :pk"\) |
| `filterExpression` | string | No | Filter expression for results |
| `expressionAttributeNames` | object | No | Attribute name mappings for reserved words |
| `expressionAttributeValues` | object | No | Expression attribute values |
| `indexName` | string | No | Secondary index name to query |
| `limit` | number | No | Maximum number of items to return |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `message` | string | Operation status message |
| `items` | array | Array of items returned |
| `count` | number | Number of items returned |
### `dynamodb_scan`
Scan all items in a DynamoDB table
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `region` | string | Yes | AWS region \(e.g., us-east-1\) |
| `accessKeyId` | string | Yes | AWS access key ID |
| `secretAccessKey` | string | Yes | AWS secret access key |
| `tableName` | string | Yes | DynamoDB table name |
| `filterExpression` | string | No | Filter expression for results |
| `projectionExpression` | string | No | Attributes to retrieve |
| `expressionAttributeNames` | object | No | Attribute name mappings for reserved words |
| `expressionAttributeValues` | object | No | Expression attribute values |
| `limit` | number | No | Maximum number of items to return |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `message` | string | Operation status message |
| `items` | array | Array of items returned |
| `count` | number | Number of items returned |
### `dynamodb_update`
Update an item in a DynamoDB table
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `region` | string | Yes | AWS region \(e.g., us-east-1\) |
| `accessKeyId` | string | Yes | AWS access key ID |
| `secretAccessKey` | string | Yes | AWS secret access key |
| `tableName` | string | Yes | DynamoDB table name |
| `key` | object | Yes | Primary key of the item to update |
| `updateExpression` | string | Yes | Update expression \(e.g., "SET #name = :name"\) |
| `expressionAttributeNames` | object | No | Attribute name mappings for reserved words |
| `expressionAttributeValues` | object | No | Expression attribute values |
| `conditionExpression` | string | No | Condition that must be met for the update to succeed |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `message` | string | Operation status message |
| `item` | object | Updated item |
### `dynamodb_delete`
Delete an item from a DynamoDB table
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `region` | string | Yes | AWS region \(e.g., us-east-1\) |
| `accessKeyId` | string | Yes | AWS access key ID |
| `secretAccessKey` | string | Yes | AWS secret access key |
| `tableName` | string | Yes | DynamoDB table name |
| `key` | object | Yes | Primary key of the item to delete |
| `conditionExpression` | string | No | Condition that must be met for the delete to succeed |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `message` | string | Operation status message |
## Notes
- Category: `tools`
- Type: `dynamodb`

View File

@@ -11,6 +11,7 @@
"clay",
"confluence",
"discord",
"dynamodb",
"elevenlabs",
"exa",
"file",
@@ -59,6 +60,7 @@
"posthog",
"pylon",
"qdrant",
"rds",
"reddit",
"resend",
"s3",

View File

@@ -0,0 +1,173 @@
---
title: Amazon RDS
description: Connect to Amazon RDS via Data API
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="rds"
color="linear-gradient(45deg, #2E27AD 0%, #527FFF 100%)"
/>
{/* MANUAL-CONTENT-START:intro */}
[Amazon RDS Aurora Serverless](https://aws.amazon.com/rds/aurora/serverless/) is a fully managed relational database that automatically starts up, shuts down, and scales capacity based on your application's needs. It allows you to run SQL databases in the cloud without managing database servers.
With RDS Aurora Serverless, you can:
- **Query data**: Run flexible SQL queries across your tables
- **Insert new records**: Add data to your database automatically
- **Update existing records**: Modify data in your tables using custom filters
- **Delete records**: Remove unwanted data using precise criteria
- **Execute raw SQL**: Run any valid SQL command supported by Aurora
In Sim, the RDS integration enables your agents to work with Amazon Aurora Serverless databases securely and programmatically. Supported operations include:
- **Query**: Run SELECT and other SQL queries to fetch rows from your database
- **Insert**: Insert new records into tables with structured data
- **Update**: Change data in rows that match your specified conditions
- **Delete**: Remove records from a table by custom filters or criteria
- **Execute**: Run raw SQL for advanced scenarios
This integration allows your agents to automate a wide range of database operations without manual intervention. By connecting Sim with Amazon RDS, you can build agents that manage, update, and retrieve relational data within your workflows—all without handling database infrastructure or connections.
{/* MANUAL-CONTENT-END */}
## Usage Instructions
Integrate Amazon RDS Aurora Serverless into the workflow using the Data API. Can query, insert, update, delete, and execute raw SQL without managing database connections.
## Tools
### `rds_query`
Execute a SELECT query on Amazon RDS using the Data API
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `region` | string | Yes | AWS region \(e.g., us-east-1\) |
| `accessKeyId` | string | Yes | AWS access key ID |
| `secretAccessKey` | string | Yes | AWS secret access key |
| `resourceArn` | string | Yes | ARN of the Aurora DB cluster |
| `secretArn` | string | Yes | ARN of the Secrets Manager secret containing DB credentials |
| `database` | string | No | Database name \(optional\) |
| `query` | string | Yes | SQL SELECT query to execute |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `message` | string | Operation status message |
| `rows` | array | Array of rows returned from the query |
| `rowCount` | number | Number of rows returned |
### `rds_insert`
Insert data into an Amazon RDS table using the Data API
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `region` | string | Yes | AWS region \(e.g., us-east-1\) |
| `accessKeyId` | string | Yes | AWS access key ID |
| `secretAccessKey` | string | Yes | AWS secret access key |
| `resourceArn` | string | Yes | ARN of the Aurora DB cluster |
| `secretArn` | string | Yes | ARN of the Secrets Manager secret containing DB credentials |
| `database` | string | No | Database name \(optional\) |
| `table` | string | Yes | Table name to insert into |
| `data` | object | Yes | Data to insert as key-value pairs |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `message` | string | Operation status message |
| `rows` | array | Array of inserted rows |
| `rowCount` | number | Number of rows inserted |
### `rds_update`
Update data in an Amazon RDS table using the Data API
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `region` | string | Yes | AWS region \(e.g., us-east-1\) |
| `accessKeyId` | string | Yes | AWS access key ID |
| `secretAccessKey` | string | Yes | AWS secret access key |
| `resourceArn` | string | Yes | ARN of the Aurora DB cluster |
| `secretArn` | string | Yes | ARN of the Secrets Manager secret containing DB credentials |
| `database` | string | No | Database name \(optional\) |
| `table` | string | Yes | Table name to update |
| `data` | object | Yes | Data to update as key-value pairs |
| `conditions` | object | Yes | Conditions for the update \(e.g., \{"id": 1\}\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `message` | string | Operation status message |
| `rows` | array | Array of updated rows |
| `rowCount` | number | Number of rows updated |
### `rds_delete`
Delete data from an Amazon RDS table using the Data API
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `region` | string | Yes | AWS region \(e.g., us-east-1\) |
| `accessKeyId` | string | Yes | AWS access key ID |
| `secretAccessKey` | string | Yes | AWS secret access key |
| `resourceArn` | string | Yes | ARN of the Aurora DB cluster |
| `secretArn` | string | Yes | ARN of the Secrets Manager secret containing DB credentials |
| `database` | string | No | Database name \(optional\) |
| `table` | string | Yes | Table name to delete from |
| `conditions` | object | Yes | Conditions for the delete \(e.g., \{"id": 1\}\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `message` | string | Operation status message |
| `rows` | array | Array of deleted rows |
| `rowCount` | number | Number of rows deleted |
### `rds_execute`
Execute raw SQL on Amazon RDS using the Data API
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `region` | string | Yes | AWS region \(e.g., us-east-1\) |
| `accessKeyId` | string | Yes | AWS access key ID |
| `secretAccessKey` | string | Yes | AWS secret access key |
| `resourceArn` | string | Yes | ARN of the Aurora DB cluster |
| `secretArn` | string | Yes | ARN of the Secrets Manager secret containing DB credentials |
| `database` | string | No | Database name \(optional\) |
| `query` | string | Yes | Raw SQL query to execute |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `message` | string | Operation status message |
| `rows` | array | Array of rows returned or affected |
| `rowCount` | number | Number of rows affected |
## Notes
- Category: `tools`
- Type: `rds`

View File

@@ -7,7 +7,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="s3"
color="#E0E0E0"
color="linear-gradient(45deg, #1B660F 0%, #6CAE3E 100%)"
/>
{/* MANUAL-CONTENT-START:intro */}

View File

@@ -1,10 +1,39 @@
'use client'
import type { SVGProps } from 'react'
import Link from 'next/link'
import { StatusDotIcon } from '@/components/icons'
import type { StatusType } from '@/app/api/status/types'
import { useStatus } from '@/hooks/queries/status'
interface StatusDotIconProps extends SVGProps<SVGSVGElement> {
status: 'operational' | 'degraded' | 'outage' | 'maintenance' | 'loading' | 'error'
}
export function StatusDotIcon({ status, className, ...props }: StatusDotIconProps) {
const colors = {
operational: '#10B981',
degraded: '#F59E0B',
outage: '#EF4444',
maintenance: '#3B82F6',
loading: '#9CA3AF',
error: '#9CA3AF',
}
return (
<svg
xmlns='http://www.w3.org/2000/svg'
width={6}
height={6}
viewBox='0 0 6 6'
fill='none'
className={className}
{...props}
>
<circle cx={3} cy={3} r={3} fill={colors[status]} />
</svg>
)
}
const STATUS_COLORS: Record<StatusType, string> = {
operational: 'text-[#10B981] hover:text-[#059669]',
degraded: 'text-[#F59E0B] hover:text-[#D97706]',

View File

@@ -0,0 +1,41 @@
import { NextResponse } from 'next/server'
import { z } from 'zod'
import { createDynamoDBClient, deleteItem } from '@/app/api/tools/dynamodb/utils'
const DeleteSchema = z.object({
region: z.string().min(1, 'AWS region is required'),
accessKeyId: z.string().min(1, 'AWS access key ID is required'),
secretAccessKey: z.string().min(1, 'AWS secret access key is required'),
tableName: z.string().min(1, 'Table name is required'),
key: z.record(z.unknown()).refine((val) => Object.keys(val).length > 0, {
message: 'Key is required',
}),
conditionExpression: z.string().optional(),
})
export async function POST(request: Request) {
try {
const body = await request.json()
const validatedData = DeleteSchema.parse(body)
const client = createDynamoDBClient({
region: validatedData.region,
accessKeyId: validatedData.accessKeyId,
secretAccessKey: validatedData.secretAccessKey,
})
await deleteItem(
client,
validatedData.tableName,
validatedData.key,
validatedData.conditionExpression
)
return NextResponse.json({
message: 'Item deleted successfully',
})
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'DynamoDB delete failed'
return NextResponse.json({ error: errorMessage }, { status: 500 })
}
}

View File

@@ -0,0 +1,48 @@
import { NextResponse } from 'next/server'
import { z } from 'zod'
import { createDynamoDBClient, getItem } from '@/app/api/tools/dynamodb/utils'
const GetSchema = z.object({
region: z.string().min(1, 'AWS region is required'),
accessKeyId: z.string().min(1, 'AWS access key ID is required'),
secretAccessKey: z.string().min(1, 'AWS secret access key is required'),
tableName: z.string().min(1, 'Table name is required'),
key: z.record(z.unknown()).refine((val) => Object.keys(val).length > 0, {
message: 'Key is required',
}),
consistentRead: z
.union([z.boolean(), z.string()])
.optional()
.transform((val) => {
if (val === true || val === 'true') return true
return undefined
}),
})
export async function POST(request: Request) {
try {
const body = await request.json()
const validatedData = GetSchema.parse(body)
const client = createDynamoDBClient({
region: validatedData.region,
accessKeyId: validatedData.accessKeyId,
secretAccessKey: validatedData.secretAccessKey,
})
const result = await getItem(
client,
validatedData.tableName,
validatedData.key,
validatedData.consistentRead
)
return NextResponse.json({
message: result.item ? 'Item retrieved successfully' : 'Item not found',
item: result.item,
})
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'DynamoDB get failed'
return NextResponse.json({ error: errorMessage }, { status: 500 })
}
}

View File

@@ -0,0 +1,36 @@
import { NextResponse } from 'next/server'
import { z } from 'zod'
import { createDynamoDBClient, putItem } from '@/app/api/tools/dynamodb/utils'
const PutSchema = z.object({
region: z.string().min(1, 'AWS region is required'),
accessKeyId: z.string().min(1, 'AWS access key ID is required'),
secretAccessKey: z.string().min(1, 'AWS secret access key is required'),
tableName: z.string().min(1, 'Table name is required'),
item: z.record(z.unknown()).refine((val) => Object.keys(val).length > 0, {
message: 'Item is required',
}),
})
export async function POST(request: Request) {
try {
const body = await request.json()
const validatedData = PutSchema.parse(body)
const client = createDynamoDBClient({
region: validatedData.region,
accessKeyId: validatedData.accessKeyId,
secretAccessKey: validatedData.secretAccessKey,
})
await putItem(client, validatedData.tableName, validatedData.item)
return NextResponse.json({
message: 'Item created successfully',
item: validatedData.item,
})
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'DynamoDB put failed'
return NextResponse.json({ error: errorMessage }, { status: 500 })
}
}

View File

@@ -0,0 +1,51 @@
import { NextResponse } from 'next/server'
import { z } from 'zod'
import { createDynamoDBClient, queryItems } from '@/app/api/tools/dynamodb/utils'
const QuerySchema = z.object({
region: z.string().min(1, 'AWS region is required'),
accessKeyId: z.string().min(1, 'AWS access key ID is required'),
secretAccessKey: z.string().min(1, 'AWS secret access key is required'),
tableName: z.string().min(1, 'Table name is required'),
keyConditionExpression: z.string().min(1, 'Key condition expression is required'),
filterExpression: z.string().optional(),
expressionAttributeNames: z.record(z.string()).optional(),
expressionAttributeValues: z.record(z.unknown()).optional(),
indexName: z.string().optional(),
limit: z.number().positive().optional(),
})
export async function POST(request: Request) {
try {
const body = await request.json()
const validatedData = QuerySchema.parse(body)
const client = createDynamoDBClient({
region: validatedData.region,
accessKeyId: validatedData.accessKeyId,
secretAccessKey: validatedData.secretAccessKey,
})
const result = await queryItems(
client,
validatedData.tableName,
validatedData.keyConditionExpression,
{
filterExpression: validatedData.filterExpression,
expressionAttributeNames: validatedData.expressionAttributeNames,
expressionAttributeValues: validatedData.expressionAttributeValues,
indexName: validatedData.indexName,
limit: validatedData.limit,
}
)
return NextResponse.json({
message: `Query returned ${result.count} items`,
items: result.items,
count: result.count,
})
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'DynamoDB query failed'
return NextResponse.json({ error: errorMessage }, { status: 500 })
}
}

View File

@@ -0,0 +1,45 @@
import { NextResponse } from 'next/server'
import { z } from 'zod'
import { createDynamoDBClient, scanItems } from '@/app/api/tools/dynamodb/utils'
const ScanSchema = z.object({
region: z.string().min(1, 'AWS region is required'),
accessKeyId: z.string().min(1, 'AWS access key ID is required'),
secretAccessKey: z.string().min(1, 'AWS secret access key is required'),
tableName: z.string().min(1, 'Table name is required'),
filterExpression: z.string().optional(),
projectionExpression: z.string().optional(),
expressionAttributeNames: z.record(z.string()).optional(),
expressionAttributeValues: z.record(z.unknown()).optional(),
limit: z.number().positive().optional(),
})
export async function POST(request: Request) {
try {
const body = await request.json()
const validatedData = ScanSchema.parse(body)
const client = createDynamoDBClient({
region: validatedData.region,
accessKeyId: validatedData.accessKeyId,
secretAccessKey: validatedData.secretAccessKey,
})
const result = await scanItems(client, validatedData.tableName, {
filterExpression: validatedData.filterExpression,
projectionExpression: validatedData.projectionExpression,
expressionAttributeNames: validatedData.expressionAttributeNames,
expressionAttributeValues: validatedData.expressionAttributeValues,
limit: validatedData.limit,
})
return NextResponse.json({
message: `Scan returned ${result.count} items`,
items: result.items,
count: result.count,
})
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'DynamoDB scan failed'
return NextResponse.json({ error: errorMessage }, { status: 500 })
}
}

View File

@@ -0,0 +1,50 @@
import { NextResponse } from 'next/server'
import { z } from 'zod'
import { createDynamoDBClient, updateItem } from '@/app/api/tools/dynamodb/utils'
const UpdateSchema = z.object({
region: z.string().min(1, 'AWS region is required'),
accessKeyId: z.string().min(1, 'AWS access key ID is required'),
secretAccessKey: z.string().min(1, 'AWS secret access key is required'),
tableName: z.string().min(1, 'Table name is required'),
key: z.record(z.unknown()).refine((val) => Object.keys(val).length > 0, {
message: 'Key is required',
}),
updateExpression: z.string().min(1, 'Update expression is required'),
expressionAttributeNames: z.record(z.string()).optional(),
expressionAttributeValues: z.record(z.unknown()).optional(),
conditionExpression: z.string().optional(),
})
export async function POST(request: Request) {
try {
const body = await request.json()
const validatedData = UpdateSchema.parse(body)
const client = createDynamoDBClient({
region: validatedData.region,
accessKeyId: validatedData.accessKeyId,
secretAccessKey: validatedData.secretAccessKey,
})
const result = await updateItem(
client,
validatedData.tableName,
validatedData.key,
validatedData.updateExpression,
{
expressionAttributeNames: validatedData.expressionAttributeNames,
expressionAttributeValues: validatedData.expressionAttributeValues,
conditionExpression: validatedData.conditionExpression,
}
)
return NextResponse.json({
message: 'Item updated successfully',
item: result.attributes,
})
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'DynamoDB update failed'
return NextResponse.json({ error: errorMessage }, { status: 500 })
}
}

View File

@@ -0,0 +1,174 @@
import { DynamoDBClient } from '@aws-sdk/client-dynamodb'
import {
DeleteCommand,
DynamoDBDocumentClient,
GetCommand,
PutCommand,
QueryCommand,
ScanCommand,
UpdateCommand,
} from '@aws-sdk/lib-dynamodb'
import type { DynamoDBConnectionConfig } from '@/tools/dynamodb/types'
export function createDynamoDBClient(config: DynamoDBConnectionConfig): DynamoDBDocumentClient {
const client = new DynamoDBClient({
region: config.region,
credentials: {
accessKeyId: config.accessKeyId,
secretAccessKey: config.secretAccessKey,
},
})
return DynamoDBDocumentClient.from(client, {
marshallOptions: {
removeUndefinedValues: true,
convertEmptyValues: false,
},
unmarshallOptions: {
wrapNumbers: false,
},
})
}
export async function getItem(
client: DynamoDBDocumentClient,
tableName: string,
key: Record<string, unknown>,
consistentRead?: boolean
): Promise<{ item: Record<string, unknown> | null }> {
const command = new GetCommand({
TableName: tableName,
Key: key,
ConsistentRead: consistentRead,
})
const response = await client.send(command)
return {
item: (response.Item as Record<string, unknown>) || null,
}
}
export async function putItem(
client: DynamoDBDocumentClient,
tableName: string,
item: Record<string, unknown>
): Promise<{ success: boolean }> {
const command = new PutCommand({
TableName: tableName,
Item: item,
})
await client.send(command)
return { success: true }
}
export async function queryItems(
client: DynamoDBDocumentClient,
tableName: string,
keyConditionExpression: string,
options?: {
filterExpression?: string
expressionAttributeNames?: Record<string, string>
expressionAttributeValues?: Record<string, unknown>
indexName?: string
limit?: number
}
): Promise<{ items: Record<string, unknown>[]; count: number }> {
const command = new QueryCommand({
TableName: tableName,
KeyConditionExpression: keyConditionExpression,
...(options?.filterExpression && { FilterExpression: options.filterExpression }),
...(options?.expressionAttributeNames && {
ExpressionAttributeNames: options.expressionAttributeNames,
}),
...(options?.expressionAttributeValues && {
ExpressionAttributeValues: options.expressionAttributeValues,
}),
...(options?.indexName && { IndexName: options.indexName }),
...(options?.limit && { Limit: options.limit }),
})
const response = await client.send(command)
return {
items: (response.Items as Record<string, unknown>[]) || [],
count: response.Count || 0,
}
}
export async function scanItems(
client: DynamoDBDocumentClient,
tableName: string,
options?: {
filterExpression?: string
projectionExpression?: string
expressionAttributeNames?: Record<string, string>
expressionAttributeValues?: Record<string, unknown>
limit?: number
}
): Promise<{ items: Record<string, unknown>[]; count: number }> {
const command = new ScanCommand({
TableName: tableName,
...(options?.filterExpression && { FilterExpression: options.filterExpression }),
...(options?.projectionExpression && { ProjectionExpression: options.projectionExpression }),
...(options?.expressionAttributeNames && {
ExpressionAttributeNames: options.expressionAttributeNames,
}),
...(options?.expressionAttributeValues && {
ExpressionAttributeValues: options.expressionAttributeValues,
}),
...(options?.limit && { Limit: options.limit }),
})
const response = await client.send(command)
return {
items: (response.Items as Record<string, unknown>[]) || [],
count: response.Count || 0,
}
}
export async function updateItem(
client: DynamoDBDocumentClient,
tableName: string,
key: Record<string, unknown>,
updateExpression: string,
options?: {
expressionAttributeNames?: Record<string, string>
expressionAttributeValues?: Record<string, unknown>
conditionExpression?: string
}
): Promise<{ attributes: Record<string, unknown> | null }> {
const command = new UpdateCommand({
TableName: tableName,
Key: key,
UpdateExpression: updateExpression,
...(options?.expressionAttributeNames && {
ExpressionAttributeNames: options.expressionAttributeNames,
}),
...(options?.expressionAttributeValues && {
ExpressionAttributeValues: options.expressionAttributeValues,
}),
...(options?.conditionExpression && { ConditionExpression: options.conditionExpression }),
ReturnValues: 'ALL_NEW',
})
const response = await client.send(command)
return {
attributes: (response.Attributes as Record<string, unknown>) || null,
}
}
export async function deleteItem(
client: DynamoDBDocumentClient,
tableName: string,
key: Record<string, unknown>,
conditionExpression?: string
): Promise<{ success: boolean }> {
const command = new DeleteCommand({
TableName: tableName,
Key: key,
...(conditionExpression && { ConditionExpression: conditionExpression }),
})
await client.send(command)
return { success: true }
}

View File

@@ -0,0 +1,74 @@
import { randomUUID } from 'crypto'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { createLogger } from '@/lib/logs/console/logger'
import { createRdsClient, executeDelete } from '@/app/api/tools/rds/utils'
const logger = createLogger('RDSDeleteAPI')
const DeleteSchema = z.object({
region: z.string().min(1, 'AWS region is required'),
accessKeyId: z.string().min(1, 'AWS access key ID is required'),
secretAccessKey: z.string().min(1, 'AWS secret access key is required'),
resourceArn: z.string().min(1, 'Resource ARN is required'),
secretArn: z.string().min(1, 'Secret ARN is required'),
database: z.string().optional(),
table: z.string().min(1, 'Table name is required'),
conditions: z.record(z.unknown()).refine((obj) => Object.keys(obj).length > 0, {
message: 'At least one condition is required',
}),
})
export async function POST(request: NextRequest) {
const requestId = randomUUID().slice(0, 8)
try {
const body = await request.json()
const params = DeleteSchema.parse(body)
logger.info(`[${requestId}] Deleting from RDS table ${params.table} in ${params.database}`)
const client = createRdsClient({
region: params.region,
accessKeyId: params.accessKeyId,
secretAccessKey: params.secretAccessKey,
resourceArn: params.resourceArn,
secretArn: params.secretArn,
database: params.database,
})
try {
const result = await executeDelete(
client,
params.resourceArn,
params.secretArn,
params.database,
params.table,
params.conditions
)
logger.info(`[${requestId}] Delete executed successfully, affected ${result.rowCount} rows`)
return NextResponse.json({
message: `Delete executed successfully. ${result.rowCount} row(s) deleted.`,
rows: result.rows,
rowCount: result.rowCount,
})
} finally {
client.destroy()
}
} catch (error) {
if (error instanceof z.ZodError) {
logger.warn(`[${requestId}] Invalid request data`, { errors: error.errors })
return NextResponse.json(
{ error: 'Invalid request data', details: error.errors },
{ status: 400 }
)
}
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'
logger.error(`[${requestId}] RDS delete failed:`, error)
return NextResponse.json({ error: `RDS delete failed: ${errorMessage}` }, { status: 500 })
}
}

View File

@@ -0,0 +1,70 @@
import { randomUUID } from 'crypto'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { createLogger } from '@/lib/logs/console/logger'
import { createRdsClient, executeStatement } from '@/app/api/tools/rds/utils'
const logger = createLogger('RDSExecuteAPI')
const ExecuteSchema = z.object({
region: z.string().min(1, 'AWS region is required'),
accessKeyId: z.string().min(1, 'AWS access key ID is required'),
secretAccessKey: z.string().min(1, 'AWS secret access key is required'),
resourceArn: z.string().min(1, 'Resource ARN is required'),
secretArn: z.string().min(1, 'Secret ARN is required'),
database: z.string().optional(),
query: z.string().min(1, 'Query is required'),
})
export async function POST(request: NextRequest) {
const requestId = randomUUID().slice(0, 8)
try {
const body = await request.json()
const params = ExecuteSchema.parse(body)
logger.info(`[${requestId}] Executing raw SQL on RDS database ${params.database}`)
const client = createRdsClient({
region: params.region,
accessKeyId: params.accessKeyId,
secretAccessKey: params.secretAccessKey,
resourceArn: params.resourceArn,
secretArn: params.secretArn,
database: params.database,
})
try {
const result = await executeStatement(
client,
params.resourceArn,
params.secretArn,
params.database,
params.query
)
logger.info(`[${requestId}] Execute completed successfully, affected ${result.rowCount} rows`)
return NextResponse.json({
message: `Query executed successfully. ${result.rowCount} row(s) affected.`,
rows: result.rows,
rowCount: result.rowCount,
})
} finally {
client.destroy()
}
} catch (error) {
if (error instanceof z.ZodError) {
logger.warn(`[${requestId}] Invalid request data`, { errors: error.errors })
return NextResponse.json(
{ error: 'Invalid request data', details: error.errors },
{ status: 400 }
)
}
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'
logger.error(`[${requestId}] RDS execute failed:`, error)
return NextResponse.json({ error: `RDS execute failed: ${errorMessage}` }, { status: 500 })
}
}

View File

@@ -0,0 +1,74 @@
import { randomUUID } from 'crypto'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { createLogger } from '@/lib/logs/console/logger'
import { createRdsClient, executeInsert } from '@/app/api/tools/rds/utils'
const logger = createLogger('RDSInsertAPI')
const InsertSchema = z.object({
region: z.string().min(1, 'AWS region is required'),
accessKeyId: z.string().min(1, 'AWS access key ID is required'),
secretAccessKey: z.string().min(1, 'AWS secret access key is required'),
resourceArn: z.string().min(1, 'Resource ARN is required'),
secretArn: z.string().min(1, 'Secret ARN is required'),
database: z.string().optional(),
table: z.string().min(1, 'Table name is required'),
data: z.record(z.unknown()).refine((obj) => Object.keys(obj).length > 0, {
message: 'Data object must have at least one field',
}),
})
export async function POST(request: NextRequest) {
const requestId = randomUUID().slice(0, 8)
try {
const body = await request.json()
const params = InsertSchema.parse(body)
logger.info(`[${requestId}] Inserting into RDS table ${params.table} in ${params.database}`)
const client = createRdsClient({
region: params.region,
accessKeyId: params.accessKeyId,
secretAccessKey: params.secretAccessKey,
resourceArn: params.resourceArn,
secretArn: params.secretArn,
database: params.database,
})
try {
const result = await executeInsert(
client,
params.resourceArn,
params.secretArn,
params.database,
params.table,
params.data
)
logger.info(`[${requestId}] Insert executed successfully, affected ${result.rowCount} rows`)
return NextResponse.json({
message: `Insert executed successfully. ${result.rowCount} row(s) inserted.`,
rows: result.rows,
rowCount: result.rowCount,
})
} finally {
client.destroy()
}
} catch (error) {
if (error instanceof z.ZodError) {
logger.warn(`[${requestId}] Invalid request data`, { errors: error.errors })
return NextResponse.json(
{ error: 'Invalid request data', details: error.errors },
{ status: 400 }
)
}
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'
logger.error(`[${requestId}] RDS insert failed:`, error)
return NextResponse.json({ error: `RDS insert failed: ${errorMessage}` }, { status: 500 })
}
}

View File

@@ -0,0 +1,77 @@
import { randomUUID } from 'crypto'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { createLogger } from '@/lib/logs/console/logger'
import { createRdsClient, executeStatement, validateQuery } from '@/app/api/tools/rds/utils'
const logger = createLogger('RDSQueryAPI')
const QuerySchema = z.object({
region: z.string().min(1, 'AWS region is required'),
accessKeyId: z.string().min(1, 'AWS access key ID is required'),
secretAccessKey: z.string().min(1, 'AWS secret access key is required'),
resourceArn: z.string().min(1, 'Resource ARN is required'),
secretArn: z.string().min(1, 'Secret ARN is required'),
database: z.string().optional(),
query: z.string().min(1, 'Query is required'),
})
export async function POST(request: NextRequest) {
const requestId = randomUUID().slice(0, 8)
try {
const body = await request.json()
const params = QuerySchema.parse(body)
logger.info(`[${requestId}] Executing RDS query on ${params.database}`)
// Validate the query
const validation = validateQuery(params.query)
if (!validation.isValid) {
logger.warn(`[${requestId}] Query validation failed: ${validation.error}`)
return NextResponse.json({ error: validation.error }, { status: 400 })
}
const client = createRdsClient({
region: params.region,
accessKeyId: params.accessKeyId,
secretAccessKey: params.secretAccessKey,
resourceArn: params.resourceArn,
secretArn: params.secretArn,
database: params.database,
})
try {
const result = await executeStatement(
client,
params.resourceArn,
params.secretArn,
params.database,
params.query
)
logger.info(`[${requestId}] Query executed successfully, returned ${result.rowCount} rows`)
return NextResponse.json({
message: `Query executed successfully. ${result.rowCount} row(s) returned.`,
rows: result.rows,
rowCount: result.rowCount,
})
} finally {
client.destroy()
}
} catch (error) {
if (error instanceof z.ZodError) {
logger.warn(`[${requestId}] Invalid request data`, { errors: error.errors })
return NextResponse.json(
{ error: 'Invalid request data', details: error.errors },
{ status: 400 }
)
}
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'
logger.error(`[${requestId}] RDS query failed:`, error)
return NextResponse.json({ error: `RDS query failed: ${errorMessage}` }, { status: 500 })
}
}

View File

@@ -0,0 +1,78 @@
import { randomUUID } from 'crypto'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { createLogger } from '@/lib/logs/console/logger'
import { createRdsClient, executeUpdate } from '@/app/api/tools/rds/utils'
const logger = createLogger('RDSUpdateAPI')
const UpdateSchema = z.object({
region: z.string().min(1, 'AWS region is required'),
accessKeyId: z.string().min(1, 'AWS access key ID is required'),
secretAccessKey: z.string().min(1, 'AWS secret access key is required'),
resourceArn: z.string().min(1, 'Resource ARN is required'),
secretArn: z.string().min(1, 'Secret ARN is required'),
database: z.string().optional(),
table: z.string().min(1, 'Table name is required'),
data: z.record(z.unknown()).refine((obj) => Object.keys(obj).length > 0, {
message: 'Data object must have at least one field',
}),
conditions: z.record(z.unknown()).refine((obj) => Object.keys(obj).length > 0, {
message: 'At least one condition is required',
}),
})
export async function POST(request: NextRequest) {
const requestId = randomUUID().slice(0, 8)
try {
const body = await request.json()
const params = UpdateSchema.parse(body)
logger.info(`[${requestId}] Updating RDS table ${params.table} in ${params.database}`)
const client = createRdsClient({
region: params.region,
accessKeyId: params.accessKeyId,
secretAccessKey: params.secretAccessKey,
resourceArn: params.resourceArn,
secretArn: params.secretArn,
database: params.database,
})
try {
const result = await executeUpdate(
client,
params.resourceArn,
params.secretArn,
params.database,
params.table,
params.data,
params.conditions
)
logger.info(`[${requestId}] Update executed successfully, affected ${result.rowCount} rows`)
return NextResponse.json({
message: `Update executed successfully. ${result.rowCount} row(s) updated.`,
rows: result.rows,
rowCount: result.rowCount,
})
} finally {
client.destroy()
}
} catch (error) {
if (error instanceof z.ZodError) {
logger.warn(`[${requestId}] Invalid request data`, { errors: error.errors })
return NextResponse.json(
{ error: 'Invalid request data', details: error.errors },
{ status: 400 }
)
}
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'
logger.error(`[${requestId}] RDS update failed:`, error)
return NextResponse.json({ error: `RDS update failed: ${errorMessage}` }, { status: 500 })
}
}

View File

@@ -0,0 +1,266 @@
import {
ExecuteStatementCommand,
type ExecuteStatementCommandOutput,
type Field,
RDSDataClient,
type SqlParameter,
} from '@aws-sdk/client-rds-data'
import type { RdsConnectionConfig } from '@/tools/rds/types'
export function createRdsClient(config: RdsConnectionConfig): RDSDataClient {
return new RDSDataClient({
region: config.region,
credentials: {
accessKeyId: config.accessKeyId,
secretAccessKey: config.secretAccessKey,
},
})
}
export async function executeStatement(
client: RDSDataClient,
resourceArn: string,
secretArn: string,
database: string | undefined,
sql: string,
parameters?: SqlParameter[]
): Promise<{ rows: Record<string, unknown>[]; rowCount: number }> {
const command = new ExecuteStatementCommand({
resourceArn,
secretArn,
...(database && { database }),
sql,
...(parameters && parameters.length > 0 && { parameters }),
includeResultMetadata: true,
})
const response = await client.send(command)
const rows = parseRdsResponse(response)
return {
rows,
rowCount: response.numberOfRecordsUpdated ?? rows.length,
}
}
function parseRdsResponse(response: ExecuteStatementCommandOutput): Record<string, unknown>[] {
if (!response.records || !response.columnMetadata) {
return []
}
const columnNames = response.columnMetadata.map((col) => col.name || col.label || 'unknown')
return response.records.map((record) => {
const row: Record<string, unknown> = {}
record.forEach((field, index) => {
const columnName = columnNames[index] || `column_${index}`
row[columnName] = parseFieldValue(field)
})
return row
})
}
function parseFieldValue(field: Field): unknown {
if (field.isNull) return null
if (field.stringValue !== undefined) return field.stringValue
if (field.longValue !== undefined) return field.longValue
if (field.doubleValue !== undefined) return field.doubleValue
if (field.booleanValue !== undefined) return field.booleanValue
if (field.blobValue !== undefined) return Buffer.from(field.blobValue).toString('base64')
if (field.arrayValue !== undefined) {
const arr = field.arrayValue
if (arr.stringValues) return arr.stringValues
if (arr.longValues) return arr.longValues
if (arr.doubleValues) return arr.doubleValues
if (arr.booleanValues) return arr.booleanValues
if (arr.arrayValues) return arr.arrayValues.map((f) => parseFieldValue({ arrayValue: f }))
return []
}
return null
}
export function validateQuery(query: string): { isValid: boolean; error?: string } {
const trimmedQuery = query.trim().toLowerCase()
const dangerousPatterns = [
/drop\s+database/i,
/drop\s+schema/i,
/drop\s+user/i,
/create\s+user/i,
/create\s+role/i,
/grant\s+/i,
/revoke\s+/i,
/alter\s+user/i,
/alter\s+role/i,
/set\s+role/i,
/reset\s+role/i,
]
for (const pattern of dangerousPatterns) {
if (pattern.test(query)) {
return {
isValid: false,
error: `Query contains potentially dangerous operation: ${pattern.source}`,
}
}
}
const allowedStatements = /^(select|insert|update|delete|with|explain|show)\s+/i
if (!allowedStatements.test(trimmedQuery)) {
return {
isValid: false,
error: 'Only SELECT, INSERT, UPDATE, DELETE, WITH, EXPLAIN, and SHOW statements are allowed',
}
}
return { isValid: true }
}
export function sanitizeIdentifier(identifier: string): string {
if (identifier.includes('.')) {
const parts = identifier.split('.')
return parts.map((part) => sanitizeSingleIdentifier(part)).join('.')
}
return sanitizeSingleIdentifier(identifier)
}
function sanitizeSingleIdentifier(identifier: string): string {
const cleaned = identifier.replace(/`/g, '').replace(/"/g, '')
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(cleaned)) {
throw new Error(
`Invalid identifier: ${identifier}. Identifiers must start with a letter or underscore and contain only letters, numbers, and underscores.`
)
}
return cleaned
}
/**
* Convert a JS value to an RDS Data API SqlParameter value
*/
function toSqlParameterValue(value: unknown): SqlParameter['value'] {
if (value === null || value === undefined) {
return { isNull: true }
}
if (typeof value === 'boolean') {
return { booleanValue: value }
}
if (typeof value === 'number') {
if (Number.isInteger(value)) {
return { longValue: value }
}
return { doubleValue: value }
}
if (typeof value === 'string') {
return { stringValue: value }
}
if (value instanceof Uint8Array || Buffer.isBuffer(value)) {
return { blobValue: value }
}
// Objects/arrays as JSON strings
return { stringValue: JSON.stringify(value) }
}
/**
* Build parameterized INSERT query
*/
export async function executeInsert(
client: RDSDataClient,
resourceArn: string,
secretArn: string,
database: string | undefined,
table: string,
data: Record<string, unknown>
): Promise<{ rows: Record<string, unknown>[]; rowCount: number }> {
const sanitizedTable = sanitizeIdentifier(table)
const columns = Object.keys(data)
const sanitizedColumns = columns.map((col) => sanitizeIdentifier(col))
const placeholders = columns.map((col) => `:${col}`)
const parameters: SqlParameter[] = columns.map((col) => ({
name: col,
value: toSqlParameterValue(data[col]),
}))
const sql = `INSERT INTO ${sanitizedTable} (${sanitizedColumns.join(', ')}) VALUES (${placeholders.join(', ')})`
return executeStatement(client, resourceArn, secretArn, database, sql, parameters)
}
/**
* Build parameterized UPDATE query with conditions
*/
export async function executeUpdate(
client: RDSDataClient,
resourceArn: string,
secretArn: string,
database: string | undefined,
table: string,
data: Record<string, unknown>,
conditions: Record<string, unknown>
): Promise<{ rows: Record<string, unknown>[]; rowCount: number }> {
const sanitizedTable = sanitizeIdentifier(table)
// Build SET clause with parameters
const dataColumns = Object.keys(data)
const setClause = dataColumns.map((col) => `${sanitizeIdentifier(col)} = :set_${col}`).join(', ')
// Build WHERE clause with parameters
const conditionColumns = Object.keys(conditions)
if (conditionColumns.length === 0) {
throw new Error('At least one condition is required for UPDATE operations')
}
const whereClause = conditionColumns
.map((col) => `${sanitizeIdentifier(col)} = :where_${col}`)
.join(' AND ')
// Build parameters array (prefixed to avoid name collisions)
const parameters: SqlParameter[] = [
...dataColumns.map((col) => ({
name: `set_${col}`,
value: toSqlParameterValue(data[col]),
})),
...conditionColumns.map((col) => ({
name: `where_${col}`,
value: toSqlParameterValue(conditions[col]),
})),
]
const sql = `UPDATE ${sanitizedTable} SET ${setClause} WHERE ${whereClause}`
return executeStatement(client, resourceArn, secretArn, database, sql, parameters)
}
/**
* Build parameterized DELETE query with conditions
*/
export async function executeDelete(
client: RDSDataClient,
resourceArn: string,
secretArn: string,
database: string | undefined,
table: string,
conditions: Record<string, unknown>
): Promise<{ rows: Record<string, unknown>[]; rowCount: number }> {
const sanitizedTable = sanitizeIdentifier(table)
// Build WHERE clause with parameters
const conditionColumns = Object.keys(conditions)
if (conditionColumns.length === 0) {
throw new Error('At least one condition is required for DELETE operations')
}
const whereClause = conditionColumns
.map((col) => `${sanitizeIdentifier(col)} = :${col}`)
.join(' AND ')
const parameters: SqlParameter[] = conditionColumns.map((col) => ({
name: col,
value: toSqlParameterValue(conditions[col]),
}))
const sql = `DELETE FROM ${sanitizedTable} WHERE ${whereClause}`
return executeStatement(client, resourceArn, secretArn, database, sql, parameters)
}

View File

@@ -232,7 +232,7 @@ function TemplateCardInner({
key={index}
className='flex h-[18px] w-[18px] flex-shrink-0 items-center justify-center rounded-[4px]'
style={{
backgroundColor: blockConfig.bgColor || 'gray',
background: blockConfig.bgColor || 'gray',
marginLeft: index > 0 ? '-4px' : '0',
}}
>
@@ -257,7 +257,7 @@ function TemplateCardInner({
key={index}
className='flex h-[18px] w-[18px] flex-shrink-0 items-center justify-center rounded-[4px]'
style={{
backgroundColor: blockConfig.bgColor || 'gray',
background: blockConfig.bgColor || 'gray',
marginLeft: index > 0 ? '-4px' : '0',
}}
>

View File

@@ -196,7 +196,7 @@ export const NoteBlock = memo(function NoteBlock({ id, data }: NodeProps<NoteBlo
<div className='flex min-w-0 flex-1 items-center gap-[10px]'>
<div
className='flex h-[24px] w-[24px] flex-shrink-0 items-center justify-center rounded-[6px]'
style={{ backgroundColor: isEnabled ? config.bgColor : 'gray' }}
style={{ background: isEnabled ? config.bgColor : 'gray' }}
>
<config.icon className='h-[16px] w-[16px] text-white' />
</div>

View File

@@ -176,7 +176,7 @@ export function MentionMenu({
icon: (
<div
className='relative flex h-[16px] w-[16px] flex-shrink-0 items-center justify-center overflow-hidden rounded-[4px]'
style={{ backgroundColor: blk.bgColor || '#6B7280' }}
style={{ background: blk.bgColor || '#6B7280' }}
>
{Icon && <Icon className='!h-[10px] !w-[10px] text-white' />}
</div>
@@ -198,7 +198,7 @@ export function MentionMenu({
icon: (
<div
className='relative flex h-[16px] w-[16px] flex-shrink-0 items-center justify-center overflow-hidden rounded-[4px]'
style={{ backgroundColor: blk.bgColor || '#6B7280' }}
style={{ background: blk.bgColor || '#6B7280' }}
>
{Icon && <Icon className='!h-[10px] !w-[10px] text-white' />}
</div>
@@ -474,7 +474,7 @@ export function MentionMenu({
>
<div
className='relative flex h-[16px] w-[16px] flex-shrink-0 items-center justify-center overflow-hidden rounded-[4px]'
style={{ backgroundColor: blk.bgColor || '#6B7280' }}
style={{ background: blk.bgColor || '#6B7280' }}
>
{Icon && <Icon className='!h-[10px] !w-[10px] text-white' />}
</div>
@@ -503,7 +503,7 @@ export function MentionMenu({
>
<div
className='relative flex h-[16px] w-[16px] flex-shrink-0 items-center justify-center overflow-hidden rounded-[4px]'
style={{ backgroundColor: blk.bgColor || '#6B7280' }}
style={{ background: blk.bgColor || '#6B7280' }}
>
{Icon && <Icon className='!h-[10px] !w-[10px] text-white' />}
</div>
@@ -680,7 +680,7 @@ export function MentionMenu({
<PopoverItem key={blk.id} onClick={() => insertBlockMention(blk)}>
<div
className='relative flex h-[16px] w-[16px] flex-shrink-0 items-center justify-center overflow-hidden rounded-[4px]'
style={{ backgroundColor: blk.bgColor || '#6B7280' }}
style={{ background: blk.bgColor || '#6B7280' }}
>
{Icon && <Icon className='!h-[10px] !w-[10px] text-white' />}
</div>
@@ -709,7 +709,7 @@ export function MentionMenu({
<PopoverItem key={blk.id} onClick={() => insertWorkflowBlockMention(blk)}>
<div
className='relative flex h-[16px] w-[16px] flex-shrink-0 items-center justify-center overflow-hidden rounded-[4px]'
style={{ backgroundColor: blk.bgColor || '#6B7280' }}
style={{ background: blk.bgColor || '#6B7280' }}
>
{Icon && <Icon className='!h-[10px] !w-[10px] text-white' />}
</div>

View File

@@ -130,7 +130,7 @@ function ConnectionItem({
>
<div
className='relative flex h-[16px] w-[16px] flex-shrink-0 items-center justify-center overflow-hidden rounded-[4px]'
style={{ backgroundColor: bgColor }}
style={{ background: bgColor }}
>
{Icon && (
<Icon

View File

@@ -3,6 +3,7 @@
import { useEffect, useMemo, useState } from 'react'
import { useParams } from 'next/navigation'
import { Tooltip } from '@/components/emcn'
import { getProviderIdFromServiceId } from '@/lib/oauth/oauth'
import { SelectorCombobox } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/selector-combobox/selector-combobox'
import { useDependsOnGate } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate'
import { useForeignCredential } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-foreign-credential'
@@ -41,8 +42,11 @@ export function ChannelSelectorInput({
const effectiveCredential = previewContextValues?.credential ?? connectedCredential
const [_channelInfo, setChannelInfo] = useState<string | null>(null)
const provider = subBlock.provider || 'slack'
const isSlack = provider === 'slack'
// Use serviceId to identify the service and derive providerId for credential lookup
const serviceId = subBlock.serviceId || ''
const effectiveProviderId = useMemo(() => getProviderIdFromServiceId(serviceId), [serviceId])
const isSlack = serviceId === 'slack'
// Central dependsOn gating
const { finalDisabled, dependsOn } = useDependsOnGate(blockId, subBlock, {
disabled,
@@ -58,7 +62,7 @@ export function ChannelSelectorInput({
// Determine if connected OAuth credential is foreign (not applicable for bot tokens)
const { isForeignCredential } = useForeignCredential(
'slack',
effectiveProviderId,
(effectiveAuthMethod as string) === 'bot_token' ? '' : (effectiveCredential as string) || ''
)
@@ -87,11 +91,11 @@ export function ChannelSelectorInput({
<Tooltip.Root>
<Tooltip.Trigger asChild>
<div className='w-full rounded border border-dashed p-4 text-center text-muted-foreground text-sm'>
Channel selector not supported for provider: {provider}
Channel selector not supported for service: {serviceId || 'unknown'}
</div>
</Tooltip.Trigger>
<Tooltip.Content side='top'>
<p>This channel selector is not yet implemented for {provider}</p>
<p>This channel selector is not yet implemented for {serviceId || 'unknown'}</p>
</Tooltip.Content>
</Tooltip.Root>
)

View File

@@ -7,7 +7,6 @@ import { createLogger } from '@/lib/logs/console/logger'
import {
getCanonicalScopesForProvider,
getProviderIdFromServiceId,
getServiceIdFromScopes,
OAUTH_PROVIDERS,
type OAuthProvider,
parseProvider,
@@ -42,23 +41,19 @@ export function CredentialSelector({
const { activeWorkflowId } = useWorkflowRegistry()
const [storeValue, setStoreValue] = useSubBlockValue<string | null>(blockId, subBlock.id)
const provider = subBlock.provider as OAuthProvider
const requiredScopes = subBlock.requiredScopes || []
const label = subBlock.placeholder || 'Select credential'
const serviceId = subBlock.serviceId
const serviceId = subBlock.serviceId || ''
const effectiveValue = isPreview && previewValue !== undefined ? previewValue : storeValue
const selectedId = typeof effectiveValue === 'string' ? effectiveValue : ''
const effectiveServiceId = useMemo(
() => serviceId || getServiceIdFromScopes(provider, requiredScopes),
[provider, requiredScopes, serviceId]
)
// serviceId is now the canonical identifier - derive provider from it
const effectiveProviderId = useMemo(
() => getProviderIdFromServiceId(effectiveServiceId),
[effectiveServiceId]
() => getProviderIdFromServiceId(serviceId) as OAuthProvider,
[serviceId]
)
const provider = effectiveProviderId
const {
data: credentials = [],
@@ -259,7 +254,7 @@ export function CredentialSelector({
toolName={getProviderName(provider)}
requiredScopes={getCanonicalScopesForProvider(effectiveProviderId)}
newScopes={missingRequiredScopes}
serviceId={effectiveServiceId}
serviceId={serviceId}
/>
)}
</>

View File

@@ -3,6 +3,7 @@
import { useMemo } from 'react'
import { useParams } from 'next/navigation'
import { Tooltip } from '@/components/emcn'
import { getProviderIdFromServiceId } from '@/lib/oauth'
import { SelectorCombobox } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/selector-combobox/selector-combobox'
import { useDependsOnGate } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate'
import { useForeignCredential } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-foreign-credential'
@@ -59,10 +60,11 @@ export function FileSelectorInput({
? ((connectedCredential as Record<string, any>).id ?? '')
: ''
const { isForeignCredential } = useForeignCredential(
subBlock.serviceId || subBlock.provider,
normalizedCredentialId
)
// Derive provider from serviceId using OAuth config (same pattern as credential-selector)
const serviceId = subBlock.serviceId || ''
const effectiveProviderId = useMemo(() => getProviderIdFromServiceId(serviceId), [serviceId])
const { isForeignCredential } = useForeignCredential(effectiveProviderId, normalizedCredentialId)
const selectorResolution = useMemo<SelectorResolution | null>(() => {
return resolveSelectorForSubBlock(subBlock, {
@@ -109,11 +111,11 @@ export function FileSelectorInput({
<Tooltip.Root>
<Tooltip.Trigger asChild>
<div className='w-full rounded border border-dashed p-4 text-center text-muted-foreground text-sm'>
File selector not supported for provider: {subBlock.provider || subBlock.serviceId}
File selector not supported for service: {serviceId || 'unknown'}
</div>
</Tooltip.Trigger>
<Tooltip.Content side='top'>
<p>This file selector is not implemented for {subBlock.provider || subBlock.serviceId}</p>
<p>This file selector is not implemented for {serviceId || 'unknown'}</p>
</Tooltip.Content>
</Tooltip.Root>
)

View File

@@ -1,6 +1,7 @@
'use client'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { getProviderIdFromServiceId } from '@/lib/oauth'
import { SelectorCombobox } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/selector-combobox/selector-combobox'
import { useDependsOnGate } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate'
import { useForeignCredential } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-foreign-credential'
@@ -30,14 +31,18 @@ export function FolderSelectorInput({
const { collaborativeSetSubblockValue } = useCollaborativeWorkflow()
const { activeWorkflowId } = useWorkflowRegistry()
const [selectedFolderId, setSelectedFolderId] = useState<string>('')
const providerKey = (subBlock.provider ?? subBlock.serviceId ?? '').toLowerCase()
const credentialProvider = subBlock.serviceId ?? subBlock.provider
// Derive provider from serviceId using OAuth config (same pattern as credential-selector)
const serviceId = subBlock.serviceId || ''
const effectiveProviderId = useMemo(() => getProviderIdFromServiceId(serviceId), [serviceId])
const providerKey = serviceId.toLowerCase()
const isCopyDestinationSelector =
subBlock.canonicalParamId === 'copyDestinationId' ||
subBlock.id === 'copyDestinationFolder' ||
subBlock.id === 'manualCopyDestinationFolder'
const { isForeignCredential } = useForeignCredential(
credentialProvider,
effectiveProviderId,
(connectedCredential as string) || ''
)

View File

@@ -3,6 +3,7 @@
import { useEffect, useMemo, useState } from 'react'
import { useParams } from 'next/navigation'
import { Tooltip } from '@/components/emcn'
import { getProviderIdFromServiceId } from '@/lib/oauth'
import { SelectorCombobox } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/selector-combobox/selector-combobox'
import { useDependsOnGate } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate'
import { useForeignCredential } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-foreign-credential'
@@ -46,8 +47,13 @@ export function ProjectSelectorInput({
const linearTeamId = previewContextValues?.teamId ?? linearTeamIdFromStore
const jiraDomain = previewContextValues?.domain ?? jiraDomainFromStore
// Derive provider from serviceId using OAuth config
const serviceId = subBlock.serviceId || ''
const effectiveProviderId = useMemo(() => getProviderIdFromServiceId(serviceId), [serviceId])
const isLinear = serviceId === 'linear'
const { isForeignCredential } = useForeignCredential(
subBlock.provider || subBlock.serviceId || 'jira',
effectiveProviderId,
(connectedCredential as string) || ''
)
const activeWorkflowId = useWorkflowRegistry((s) => s.activeWorkflowId) as string | null
@@ -58,10 +64,6 @@ export function ProjectSelectorInput({
previewContextValues,
})
// Get provider-specific values
const provider = subBlock.provider || 'jira'
const isLinear = provider === 'linear'
// Jira/Discord upstream fields - use values from previewContextValues or store
const jiraCredential = connectedCredential
const domain = (jiraDomain as string) || ''
@@ -121,7 +123,7 @@ export function ProjectSelectorInput({
/>
) : (
<div className='w-full rounded border border-dashed p-4 text-center text-muted-foreground text-sm'>
Project selector not supported for provider: {subBlock.provider || 'unknown'}
Project selector not supported for service: {serviceId}
</div>
)}
</div>

View File

@@ -84,7 +84,7 @@ export function McpToolsList({
>
<div
className='flex h-[15px] w-[15px] flex-shrink-0 items-center justify-center rounded'
style={{ backgroundColor: mcpTool.bgColor }}
style={{ background: mcpTool.bgColor }}
>
<IconComponent icon={mcpTool.icon} className='h-[11px] w-[11px] text-white' />
</div>

View File

@@ -18,6 +18,7 @@ import { Toggle } from '@/components/ui/toggle'
import { createLogger } from '@/lib/logs/console/logger'
import {
getCanonicalScopesForProvider,
getProviderIdFromServiceId,
type OAuthProvider,
type OAuthService,
} from '@/lib/oauth/oauth'
@@ -168,7 +169,6 @@ function FileSelectorSyncWrapper({
id: paramId,
type: 'file-selector' as const,
title: paramId,
provider: uiComponent.provider,
serviceId: uiComponent.serviceId,
mimeType: uiComponent.mimeType,
requiredScopes: uiComponent.requiredScopes || [],
@@ -467,7 +467,7 @@ function ChannelSelectorSyncWrapper({
id: paramId,
type: 'channel-selector' as const,
title: paramId,
provider: uiComponent.provider || 'slack',
serviceId: uiComponent.serviceId,
placeholder: uiComponent.placeholder,
dependsOn: uiComponent.dependsOn,
}}
@@ -1404,7 +1404,6 @@ export function ToolInput({
id: `tool-${toolIndex || 0}-${param.id}`,
type: 'project-selector' as const,
title: param.id,
provider: uiComponent.provider || 'jira',
serviceId: uiComponent.serviceId,
placeholder: uiComponent.placeholder,
requiredScopes: uiComponent.requiredScopes,
@@ -1421,7 +1420,7 @@ export function ToolInput({
<ToolCredentialSelector
value={value}
onChange={onChange}
provider={(uiComponent.provider || uiComponent.serviceId) as OAuthProvider}
provider={getProviderIdFromServiceId(uiComponent.serviceId || '') as OAuthProvider}
serviceId={uiComponent.serviceId as OAuthService}
disabled={disabled}
requiredScopes={uiComponent.requiredScopes || []}
@@ -1729,7 +1728,7 @@ export function ToolInput({
>
<div
className='flex h-[15px] w-[15px] flex-shrink-0 items-center justify-center rounded'
style={{ backgroundColor: block.bgColor }}
style={{ background: block.bgColor }}
>
<IconComponent
icon={block.icon}
@@ -2258,7 +2257,7 @@ export function ToolInput({
>
<div
className='flex h-[15px] w-[15px] flex-shrink-0 items-center justify-center rounded'
style={{ backgroundColor: block.bgColor }}
style={{ background: block.bgColor }}
>
<IconComponent
icon={block.icon}

View File

@@ -346,10 +346,11 @@ function SubBlockComponent({
| undefined
// Use dependsOn gating to compute final disabled state
// Only pass previewContextValues when in preview mode to avoid format mismatches
const { finalDisabled: gatedDisabled } = useDependsOnGate(blockId, config, {
disabled,
isPreview,
previewContextValues: subBlockValues,
previewContextValues: isPreview ? subBlockValues : undefined,
})
const isDisabled = gatedDisabled
@@ -611,7 +612,7 @@ function SubBlockComponent({
disabled={isDisabled}
isPreview={isPreview}
previewValue={previewValue}
previewContextValues={subBlockValues}
previewContextValues={isPreview ? subBlockValues : undefined}
/>
)

View File

@@ -188,7 +188,7 @@ export function Editor() {
{(blockConfig || isSubflow) && (
<div
className='flex h-[18px] w-[18px] items-center justify-center rounded-[4px]'
style={{ backgroundColor: isSubflow ? subflowBgColor : blockConfig?.bgColor }}
style={{ background: isSubflow ? subflowBgColor : blockConfig?.bgColor }}
>
<IconComponent
icon={isSubflow ? subflowIcon : blockConfig?.icon}
@@ -353,7 +353,6 @@ export function Editor() {
blockId={currentBlockId}
config={subBlock}
isPreview={false}
subBlockValues={subBlockState}
disabled={!userPermissions.canEdit}
fieldDiffStatus={undefined}
allowExpandInPreview={false}

View File

@@ -3,12 +3,13 @@
import { useMemo } from 'react'
import { useParams } from 'next/navigation'
import { Tooltip } from '@/components/emcn'
import { getProviderIdFromServiceId } from '@/lib/oauth'
import { SelectorCombobox } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/selector-combobox/selector-combobox'
import { useDependsOnGate } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate'
import { useForeignCredential } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-foreign-credential'
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
import type { SubBlockConfig } from '@/blocks/types'
import type { SelectorContext, SelectorKey } from '@/hooks/selectors/types'
import { resolveSelectorForSubBlock, type SelectorResolution } from '@/hooks/selectors/resolution'
import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
@@ -59,27 +60,23 @@ export function FileSelectorInput({
? ((connectedCredential as Record<string, any>).id ?? '')
: ''
const { isForeignCredential } = useForeignCredential(
subBlock.provider || subBlock.serviceId || 'google-drive',
normalizedCredentialId
)
// Derive provider from serviceId using OAuth config
const serviceId = subBlock.serviceId || ''
const effectiveProviderId = useMemo(() => getProviderIdFromServiceId(serviceId), [serviceId])
const selectorResolution = useMemo(() => {
return resolveSelector({
provider: subBlock.provider || '',
serviceId: subBlock.serviceId,
mimeType: subBlock.mimeType,
const { isForeignCredential } = useForeignCredential(effectiveProviderId, normalizedCredentialId)
const selectorResolution = useMemo<SelectorResolution | null>(() => {
return resolveSelectorForSubBlock(subBlock, {
credentialId: normalizedCredentialId,
workflowId: workflowIdFromUrl,
domain: (domainValue as string) || '',
projectId: (projectIdValue as string) || '',
planId: (planIdValue as string) || '',
teamId: (teamIdValue as string) || '',
domain: (domainValue as string) || undefined,
projectId: (projectIdValue as string) || undefined,
planId: (planIdValue as string) || undefined,
teamId: (teamIdValue as string) || undefined,
})
}, [
subBlock.provider,
subBlock.serviceId,
subBlock.mimeType,
subBlock,
normalizedCredentialId,
workflowIdFromUrl,
domainValue,
@@ -90,15 +87,15 @@ export function FileSelectorInput({
const missingCredential = !normalizedCredentialId
const missingDomain =
selectorResolution.key &&
selectorResolution?.key &&
(selectorResolution.key === 'confluence.pages' || selectorResolution.key === 'jira.issues') &&
!selectorResolution.context.domain
const missingProject =
selectorResolution.key === 'jira.issues' &&
selectorResolution?.key === 'jira.issues' &&
subBlock.dependsOn?.includes('projectId') &&
!selectorResolution.context.projectId
!selectorResolution?.context.projectId
const missingPlan =
selectorResolution.key === 'microsoft.planner' && !selectorResolution.context.planId
selectorResolution?.key === 'microsoft.planner' && !selectorResolution?.context.planId
const disabledReason =
finalDisabled ||
@@ -107,18 +104,18 @@ export function FileSelectorInput({
missingDomain ||
missingProject ||
missingPlan ||
selectorResolution.key === null
!selectorResolution?.key
if (selectorResolution.key === null) {
if (!selectorResolution?.key) {
return (
<Tooltip.Root>
<Tooltip.Trigger asChild>
<div className='w-full rounded border border-dashed p-4 text-center text-muted-foreground text-sm'>
File selector not supported for provider: {subBlock.provider || subBlock.serviceId}
File selector not supported for service: {serviceId}
</div>
</Tooltip.Trigger>
<Tooltip.Content side='top'>
<p>This file selector is not implemented for {subBlock.provider || subBlock.serviceId}</p>
<p>This file selector is not implemented for {serviceId}</p>
</Tooltip.Content>
</Tooltip.Root>
)
@@ -143,69 +140,3 @@ export function FileSelectorInput({
/>
)
}
interface SelectorParams {
provider: string
serviceId?: string
mimeType?: string
credentialId: string
workflowId: string
domain?: string
projectId?: string
planId?: string
teamId?: string
}
function resolveSelector(params: SelectorParams): {
key: SelectorKey | null
context: SelectorContext
allowSearch: boolean
} {
const baseContext: SelectorContext = {
credentialId: params.credentialId,
workflowId: params.workflowId,
domain: params.domain,
projectId: params.projectId,
planId: params.planId,
teamId: params.teamId,
mimeType: params.mimeType,
}
switch (params.provider) {
case 'google-calendar':
return { key: 'google.calendar', context: baseContext, allowSearch: false }
case 'confluence':
return { key: 'confluence.pages', context: baseContext, allowSearch: true }
case 'jira':
return { key: 'jira.issues', context: baseContext, allowSearch: true }
case 'microsoft-teams':
return { key: 'microsoft.teams', context: baseContext, allowSearch: true }
case 'wealthbox':
return { key: 'wealthbox.contacts', context: baseContext, allowSearch: true }
case 'microsoft-planner':
return { key: 'microsoft.planner', context: baseContext, allowSearch: true }
case 'microsoft-excel':
return { key: 'microsoft.excel', context: baseContext, allowSearch: true }
case 'microsoft-word':
return { key: 'microsoft.word', context: baseContext, allowSearch: true }
case 'google-drive':
return { key: 'google.drive', context: baseContext, allowSearch: true }
default:
break
}
if (params.serviceId === 'onedrive') {
const key: SelectorKey = params.mimeType === 'file' ? 'onedrive.files' : 'onedrive.folders'
return { key, context: baseContext, allowSearch: true }
}
if (params.serviceId === 'sharepoint') {
return { key: 'sharepoint.sites', context: baseContext, allowSearch: true }
}
if (params.serviceId === 'google-drive') {
return { key: 'google.drive', context: baseContext, allowSearch: true }
}
return { key: null, context: baseContext, allowSearch: true }
}

View File

@@ -570,7 +570,7 @@ export const Toolbar = forwardRef<ToolbarRef, ToolbarProps>(function Toolbar(
>
<div
className='relative flex h-[16px] w-[16px] flex-shrink-0 items-center justify-center overflow-hidden rounded-[4px]'
style={{ backgroundColor: trigger.bgColor }}
style={{ background: trigger.bgColor }}
>
{Icon && (
<Icon
@@ -659,7 +659,7 @@ export const Toolbar = forwardRef<ToolbarRef, ToolbarProps>(function Toolbar(
>
<div
className='relative flex h-[16px] w-[16px] flex-shrink-0 items-center justify-center overflow-hidden rounded-[4px]'
style={{ backgroundColor: block.bgColor }}
style={{ background: block.bgColor }}
>
{Icon && (
<Icon

View File

@@ -6,6 +6,7 @@ import { Tooltip } from '@/components/emcn/components/tooltip/tooltip'
import { getEnv, isTruthy } from '@/lib/env'
import { createLogger } from '@/lib/logs/console/logger'
import { createMcpToolId } from '@/lib/mcp/utils'
import { getProviderIdFromServiceId } from '@/lib/oauth'
import { cn } from '@/lib/utils'
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
import {
@@ -272,9 +273,12 @@ const SubBlockRow = ({
const credentialSourceId =
subBlock?.type === 'oauth-input' && typeof rawValue === 'string' ? rawValue : undefined
const credentialProviderId = subBlock?.serviceId
? getProviderIdFromServiceId(subBlock.serviceId)
: undefined
const { displayName: credentialName } = useCredentialName(
credentialSourceId,
subBlock?.provider,
credentialProviderId,
workflowId
)
@@ -844,7 +848,7 @@ export const WorkflowBlock = memo(function WorkflowBlock({
<div
className='flex h-[24px] w-[24px] flex-shrink-0 items-center justify-center rounded-[6px]'
style={{
backgroundColor: isEnabled ? config.bgColor : 'gray',
background: isEnabled ? config.bgColor : 'gray',
}}
>
<config.icon className='h-[16px] w-[16px] text-white' />

View File

@@ -32,7 +32,6 @@ export const AirtableBlock: BlockConfig<AirtableResponse> = {
id: 'credential',
title: 'Airtable Account',
type: 'oauth-input',
provider: 'airtable',
serviceId: 'airtable',
requiredScopes: [
'data.records:read',

View File

@@ -34,7 +34,6 @@ export const AsanaBlock: BlockConfig<AsanaResponse> = {
type: 'oauth-input',
required: true,
provider: 'asana',
serviceId: 'asana',
requiredScopes: ['default'],
placeholder: 'Select Asana account',

View File

@@ -48,7 +48,6 @@ export const ConfluenceBlock: BlockConfig<ConfluenceResponse> = {
id: 'credential',
title: 'Confluence Account',
type: 'oauth-input',
provider: 'confluence',
serviceId: 'confluence',
requiredScopes: [
'read:confluence-content.all',
@@ -82,7 +81,6 @@ export const ConfluenceBlock: BlockConfig<ConfluenceResponse> = {
title: 'Select Page',
type: 'file-selector',
canonicalParamId: 'pageId',
provider: 'confluence',
serviceId: 'confluence',
placeholder: 'Select Confluence page',
dependsOn: ['credential', 'domain'],

View File

@@ -71,7 +71,6 @@ export const DiscordBlock: BlockConfig<DiscordResponse> = {
type: 'short-input',
placeholder: 'Enter Discord server ID',
required: true,
provider: 'discord',
serviceId: 'discord',
},
// Channel ID - for operations that need it
@@ -81,7 +80,6 @@ export const DiscordBlock: BlockConfig<DiscordResponse> = {
type: 'short-input',
placeholder: 'Enter Discord channel ID',
required: true,
provider: 'discord',
serviceId: 'discord',
condition: {
field: 'operation',

View File

@@ -0,0 +1,372 @@
import { DynamoDBIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types'
import type { DynamoDBResponse } from '@/tools/dynamodb/types'
export const DynamoDBBlock: BlockConfig<DynamoDBResponse> = {
type: 'dynamodb',
name: 'Amazon DynamoDB',
description: 'Connect to Amazon DynamoDB',
longDescription:
'Integrate Amazon DynamoDB into workflows. Supports Get, Put, Query, Scan, Update, and Delete operations on DynamoDB tables.',
docsLink: 'https://docs.sim.ai/tools/dynamodb',
category: 'tools',
bgColor: 'linear-gradient(45deg, #2E27AD 0%, #527FFF 100%)',
icon: DynamoDBIcon,
subBlocks: [
{
id: 'operation',
title: 'Operation',
type: 'dropdown',
options: [
{ label: 'Get Item', id: 'get' },
{ label: 'Put Item', id: 'put' },
{ label: 'Query', id: 'query' },
{ label: 'Scan', id: 'scan' },
{ label: 'Update Item', id: 'update' },
{ label: 'Delete Item', id: 'delete' },
],
value: () => 'get',
},
{
id: 'region',
title: 'AWS Region',
type: 'short-input',
placeholder: 'us-east-1',
required: true,
},
{
id: 'accessKeyId',
title: 'AWS Access Key ID',
type: 'short-input',
placeholder: 'AKIA...',
password: true,
required: true,
},
{
id: 'secretAccessKey',
title: 'AWS Secret Access Key',
type: 'short-input',
placeholder: 'Your secret access key',
password: true,
required: true,
},
{
id: 'tableName',
title: 'Table Name',
type: 'short-input',
placeholder: 'my-table',
required: true,
},
// Key field for get, update, delete operations
{
id: 'key',
title: 'Key (JSON)',
type: 'code',
placeholder: '{\n "pk": "user#123"\n}',
condition: { field: 'operation', value: 'get' },
required: true,
},
{
id: 'key',
title: 'Key (JSON)',
type: 'code',
placeholder: '{\n "pk": "user#123"\n}',
condition: { field: 'operation', value: 'update' },
required: true,
},
{
id: 'key',
title: 'Key (JSON)',
type: 'code',
placeholder: '{\n "pk": "user#123"\n}',
condition: { field: 'operation', value: 'delete' },
required: true,
},
// Consistent read for get
{
id: 'consistentRead',
title: 'Consistent Read',
type: 'dropdown',
options: [
{ label: 'Eventually Consistent', id: 'false' },
{ label: 'Strongly Consistent', id: 'true' },
],
value: () => 'false',
condition: { field: 'operation', value: 'get' },
},
// Item for put operation
{
id: 'item',
title: 'Item (JSON)',
type: 'code',
placeholder:
'{\n "pk": "user#123",\n "name": "John Doe",\n "email": "john@example.com"\n}',
condition: { field: 'operation', value: 'put' },
required: true,
},
// Key condition expression for query
{
id: 'keyConditionExpression',
title: 'Key Condition Expression',
type: 'short-input',
placeholder: 'pk = :pk',
condition: { field: 'operation', value: 'query' },
required: true,
},
// Update expression for update operation
{
id: 'updateExpression',
title: 'Update Expression',
type: 'short-input',
placeholder: 'SET #name = :name',
condition: { field: 'operation', value: 'update' },
required: true,
},
// Filter expression for query and scan
{
id: 'filterExpression',
title: 'Filter Expression',
type: 'short-input',
placeholder: 'attribute_exists(email)',
condition: { field: 'operation', value: 'query' },
},
{
id: 'filterExpression',
title: 'Filter Expression',
type: 'short-input',
placeholder: 'attribute_exists(email)',
condition: { field: 'operation', value: 'scan' },
},
// Projection expression for scan
{
id: 'projectionExpression',
title: 'Projection Expression',
type: 'short-input',
placeholder: 'pk, #name, email',
condition: { field: 'operation', value: 'scan' },
},
// Expression attribute names for query, scan, update
{
id: 'expressionAttributeNames',
title: 'Expression Attribute Names (JSON)',
type: 'code',
placeholder: '{\n "#name": "name"\n}',
condition: { field: 'operation', value: 'query' },
},
{
id: 'expressionAttributeNames',
title: 'Expression Attribute Names (JSON)',
type: 'code',
placeholder: '{\n "#name": "name"\n}',
condition: { field: 'operation', value: 'scan' },
},
{
id: 'expressionAttributeNames',
title: 'Expression Attribute Names (JSON)',
type: 'code',
placeholder: '{\n "#name": "name"\n}',
condition: { field: 'operation', value: 'update' },
},
// Expression attribute values for query, scan, update
{
id: 'expressionAttributeValues',
title: 'Expression Attribute Values (JSON)',
type: 'code',
placeholder: '{\n ":pk": "user#123",\n ":name": "Jane"\n}',
condition: { field: 'operation', value: 'query' },
},
{
id: 'expressionAttributeValues',
title: 'Expression Attribute Values (JSON)',
type: 'code',
placeholder: '{\n ":status": "active"\n}',
condition: { field: 'operation', value: 'scan' },
},
{
id: 'expressionAttributeValues',
title: 'Expression Attribute Values (JSON)',
type: 'code',
placeholder: '{\n ":name": "Jane Doe"\n}',
condition: { field: 'operation', value: 'update' },
},
// Index name for query
{
id: 'indexName',
title: 'Index Name',
type: 'short-input',
placeholder: 'GSI1',
condition: { field: 'operation', value: 'query' },
},
// Limit for query and scan
{
id: 'limit',
title: 'Limit',
type: 'short-input',
placeholder: '100',
condition: { field: 'operation', value: 'query' },
},
{
id: 'limit',
title: 'Limit',
type: 'short-input',
placeholder: '100',
condition: { field: 'operation', value: 'scan' },
},
// Condition expression for update and delete
{
id: 'conditionExpression',
title: 'Condition Expression',
type: 'short-input',
placeholder: 'attribute_exists(pk)',
condition: { field: 'operation', value: 'update' },
},
{
id: 'conditionExpression',
title: 'Condition Expression',
type: 'short-input',
placeholder: 'attribute_exists(pk)',
condition: { field: 'operation', value: 'delete' },
},
],
tools: {
access: [
'dynamodb_get',
'dynamodb_put',
'dynamodb_query',
'dynamodb_scan',
'dynamodb_update',
'dynamodb_delete',
],
config: {
tool: (params) => {
switch (params.operation) {
case 'get':
return 'dynamodb_get'
case 'put':
return 'dynamodb_put'
case 'query':
return 'dynamodb_query'
case 'scan':
return 'dynamodb_scan'
case 'update':
return 'dynamodb_update'
case 'delete':
return 'dynamodb_delete'
default:
throw new Error(`Invalid DynamoDB operation: ${params.operation}`)
}
},
params: (params) => {
const {
operation,
key,
item,
expressionAttributeNames,
expressionAttributeValues,
consistentRead,
limit,
...rest
} = params
// Parse JSON fields
const parseJson = (value: unknown, fieldName: string) => {
if (!value) return undefined
if (typeof value === 'object') return value
if (typeof value === 'string' && value.trim()) {
try {
return JSON.parse(value)
} catch (parseError) {
const errorMsg =
parseError instanceof Error ? parseError.message : 'Unknown JSON error'
throw new Error(`Invalid JSON in ${fieldName}: ${errorMsg}`)
}
}
return undefined
}
const parsedKey = parseJson(key, 'key')
const parsedItem = parseJson(item, 'item')
const parsedExpressionAttributeNames = parseJson(
expressionAttributeNames,
'expressionAttributeNames'
)
const parsedExpressionAttributeValues = parseJson(
expressionAttributeValues,
'expressionAttributeValues'
)
// Build connection config
const connectionConfig = {
region: rest.region,
accessKeyId: rest.accessKeyId,
secretAccessKey: rest.secretAccessKey,
}
// Build params object
const result: Record<string, unknown> = {
...connectionConfig,
tableName: rest.tableName,
}
if (parsedKey !== undefined) result.key = parsedKey
if (parsedItem !== undefined) result.item = parsedItem
if (rest.keyConditionExpression) result.keyConditionExpression = rest.keyConditionExpression
if (rest.updateExpression) result.updateExpression = rest.updateExpression
if (rest.filterExpression) result.filterExpression = rest.filterExpression
if (rest.projectionExpression) result.projectionExpression = rest.projectionExpression
if (parsedExpressionAttributeNames !== undefined) {
result.expressionAttributeNames = parsedExpressionAttributeNames
}
if (parsedExpressionAttributeValues !== undefined) {
result.expressionAttributeValues = parsedExpressionAttributeValues
}
if (rest.indexName) result.indexName = rest.indexName
if (limit) result.limit = Number.parseInt(String(limit), 10)
if (rest.conditionExpression) result.conditionExpression = rest.conditionExpression
// Handle consistentRead - dropdown sends 'true'/'false' strings or boolean
if (consistentRead === 'true' || consistentRead === true) {
result.consistentRead = true
}
return result
},
},
},
inputs: {
operation: { type: 'string', description: 'DynamoDB operation to perform' },
region: { type: 'string', description: 'AWS region' },
accessKeyId: { type: 'string', description: 'AWS access key ID' },
secretAccessKey: { type: 'string', description: 'AWS secret access key' },
tableName: { type: 'string', description: 'DynamoDB table name' },
key: { type: 'json', description: 'Primary key for get/update/delete operations' },
item: { type: 'json', description: 'Item to put into the table' },
keyConditionExpression: { type: 'string', description: 'Key condition for query operations' },
updateExpression: { type: 'string', description: 'Update expression for update operations' },
filterExpression: { type: 'string', description: 'Filter expression for query/scan' },
projectionExpression: { type: 'string', description: 'Attributes to retrieve in scan' },
expressionAttributeNames: { type: 'json', description: 'Attribute name mappings' },
expressionAttributeValues: { type: 'json', description: 'Expression attribute values' },
indexName: { type: 'string', description: 'Secondary index name for query' },
limit: { type: 'number', description: 'Maximum items to return' },
conditionExpression: { type: 'string', description: 'Condition for update/delete' },
consistentRead: { type: 'string', description: 'Use strongly consistent read' },
},
outputs: {
message: {
type: 'string',
description: 'Success or error message describing the operation outcome',
},
item: {
type: 'json',
description: 'Single item returned from get or update operation',
},
items: {
type: 'array',
description: 'Array of items returned from query or scan',
},
count: {
type: 'number',
description: 'Number of items returned',
},
},
}

View File

@@ -43,7 +43,6 @@ export const GmailBlock: BlockConfig<GmailToolResponse> = {
id: 'credential',
title: 'Gmail Account',
type: 'oauth-input',
provider: 'google-email',
serviceId: 'gmail',
requiredScopes: [
'https://www.googleapis.com/auth/gmail.send',
@@ -157,7 +156,6 @@ export const GmailBlock: BlockConfig<GmailToolResponse> = {
title: 'Label',
type: 'folder-selector',
canonicalParamId: 'folder',
provider: 'google-email',
serviceId: 'gmail',
requiredScopes: ['https://www.googleapis.com/auth/gmail.labels'],
placeholder: 'Select Gmail label/folder',
@@ -232,7 +230,6 @@ export const GmailBlock: BlockConfig<GmailToolResponse> = {
title: 'Move To Label',
type: 'folder-selector',
canonicalParamId: 'addLabelIds',
provider: 'google-email',
serviceId: 'gmail',
requiredScopes: ['https://www.googleapis.com/auth/gmail.labels'],
placeholder: 'Select destination label',
@@ -258,7 +255,6 @@ export const GmailBlock: BlockConfig<GmailToolResponse> = {
title: 'Remove From Label',
type: 'folder-selector',
canonicalParamId: 'removeLabelIds',
provider: 'google-email',
serviceId: 'gmail',
requiredScopes: ['https://www.googleapis.com/auth/gmail.labels'],
placeholder: 'Select label to remove',
@@ -311,7 +307,6 @@ export const GmailBlock: BlockConfig<GmailToolResponse> = {
title: 'Label',
type: 'folder-selector',
canonicalParamId: 'labelIds',
provider: 'google-email',
serviceId: 'gmail',
requiredScopes: ['https://www.googleapis.com/auth/gmail.labels'],
placeholder: 'Select label',

View File

@@ -33,7 +33,6 @@ export const GoogleCalendarBlock: BlockConfig<GoogleCalendarResponse> = {
title: 'Google Calendar Account',
type: 'oauth-input',
required: true,
provider: 'google-calendar',
serviceId: 'google-calendar',
requiredScopes: ['https://www.googleapis.com/auth/calendar'],
placeholder: 'Select Google Calendar account',
@@ -44,7 +43,6 @@ export const GoogleCalendarBlock: BlockConfig<GoogleCalendarResponse> = {
title: 'Calendar',
type: 'file-selector',
canonicalParamId: 'calendarId',
provider: 'google-calendar',
serviceId: 'google-calendar',
requiredScopes: ['https://www.googleapis.com/auth/calendar'],
placeholder: 'Select calendar',

View File

@@ -33,7 +33,6 @@ export const GoogleDocsBlock: BlockConfig<GoogleDocsResponse> = {
title: 'Google Account',
type: 'oauth-input',
required: true,
provider: 'google-docs',
serviceId: 'google-docs',
requiredScopes: [
'https://www.googleapis.com/auth/drive.file',
@@ -47,7 +46,6 @@ export const GoogleDocsBlock: BlockConfig<GoogleDocsResponse> = {
title: 'Select Document',
type: 'file-selector',
canonicalParamId: 'documentId',
provider: 'google-docs',
serviceId: 'google-docs',
requiredScopes: [],
mimeType: 'application/vnd.google-apps.document',
@@ -82,7 +80,6 @@ export const GoogleDocsBlock: BlockConfig<GoogleDocsResponse> = {
title: 'Select Parent Folder',
type: 'file-selector',
canonicalParamId: 'folderId',
provider: 'google-docs',
serviceId: 'google-docs',
requiredScopes: [],
mimeType: 'application/vnd.google-apps.folder',

View File

@@ -34,7 +34,6 @@ export const GoogleDriveBlock: BlockConfig<GoogleDriveResponse> = {
title: 'Google Drive Account',
type: 'oauth-input',
required: true,
provider: 'google-drive',
serviceId: 'google-drive',
requiredScopes: [
'https://www.googleapis.com/auth/drive.file',
@@ -104,7 +103,6 @@ export const GoogleDriveBlock: BlockConfig<GoogleDriveResponse> = {
title: 'Select Parent Folder',
type: 'file-selector',
canonicalParamId: 'folderId',
provider: 'google-drive',
serviceId: 'google-drive',
requiredScopes: [
'https://www.googleapis.com/auth/drive.file',
@@ -177,7 +175,6 @@ export const GoogleDriveBlock: BlockConfig<GoogleDriveResponse> = {
title: 'Select Parent Folder',
type: 'file-selector',
canonicalParamId: 'folderId',
provider: 'google-drive',
serviceId: 'google-drive',
requiredScopes: [
'https://www.googleapis.com/auth/drive.file',
@@ -205,7 +202,6 @@ export const GoogleDriveBlock: BlockConfig<GoogleDriveResponse> = {
title: 'Select Folder',
type: 'file-selector',
canonicalParamId: 'folderId',
provider: 'google-drive',
serviceId: 'google-drive',
requiredScopes: [
'https://www.googleapis.com/auth/drive.file',
@@ -247,7 +243,6 @@ export const GoogleDriveBlock: BlockConfig<GoogleDriveResponse> = {
title: 'Select File',
type: 'file-selector',
canonicalParamId: 'fileId',
provider: 'google-drive',
serviceId: 'google-drive',
requiredScopes: [
'https://www.googleapis.com/auth/drive.file',

View File

@@ -18,7 +18,6 @@ export const GoogleFormsBlock: BlockConfig = {
title: 'Google Account',
type: 'oauth-input',
required: true,
provider: 'google-forms',
serviceId: 'google-forms',
requiredScopes: [
'https://www.googleapis.com/auth/userinfo.email',

View File

@@ -34,7 +34,6 @@ export const GoogleSheetsBlock: BlockConfig<GoogleSheetsResponse> = {
title: 'Google Account',
type: 'oauth-input',
required: true,
provider: 'google-sheets',
serviceId: 'google-sheets',
requiredScopes: [
'https://www.googleapis.com/auth/drive.file',
@@ -48,7 +47,6 @@ export const GoogleSheetsBlock: BlockConfig<GoogleSheetsResponse> = {
title: 'Select Sheet',
type: 'file-selector',
canonicalParamId: 'spreadsheetId',
provider: 'google-sheets',
serviceId: 'google-sheets',
requiredScopes: [
'https://www.googleapis.com/auth/drive.file',

View File

@@ -35,7 +35,6 @@ export const GoogleVaultBlock: BlockConfig = {
title: 'Google Vault Account',
type: 'oauth-input',
required: true,
provider: 'google-vault',
serviceId: 'google-vault',
requiredScopes: [
'https://www.googleapis.com/auth/ediscovery',

View File

@@ -39,7 +39,6 @@ export const HubSpotBlock: BlockConfig<HubSpotResponse> = {
id: 'credential',
title: 'HubSpot Account',
type: 'oauth-input',
provider: 'hubspot',
serviceId: 'hubspot',
requiredScopes: [
'crm.objects.contacts.read',

View File

@@ -58,7 +58,6 @@ export const JiraBlock: BlockConfig<JiraResponse> = {
title: 'Jira Account',
type: 'oauth-input',
required: true,
provider: 'jira',
serviceId: 'jira',
requiredScopes: [
'read:jira-work',
@@ -100,7 +99,6 @@ export const JiraBlock: BlockConfig<JiraResponse> = {
title: 'Select Project',
type: 'project-selector',
canonicalParamId: 'projectId',
provider: 'jira',
serviceId: 'jira',
placeholder: 'Select Jira project',
dependsOn: ['credential', 'domain'],
@@ -122,7 +120,6 @@ export const JiraBlock: BlockConfig<JiraResponse> = {
title: 'Select Issue',
type: 'file-selector',
canonicalParamId: 'issueKey',
provider: 'jira',
serviceId: 'jira',
placeholder: 'Select Jira issue',
dependsOn: ['credential', 'domain', 'projectId'],

View File

@@ -129,7 +129,6 @@ export const LinearBlock: BlockConfig<LinearResponse> = {
id: 'credential',
title: 'Linear Account',
type: 'oauth-input',
provider: 'linear',
serviceId: 'linear',
requiredScopes: ['read', 'write'],
placeholder: 'Select Linear account',
@@ -141,7 +140,6 @@ export const LinearBlock: BlockConfig<LinearResponse> = {
title: 'Team',
type: 'project-selector',
canonicalParamId: 'teamId',
provider: 'linear',
serviceId: 'linear',
placeholder: 'Select a team',
dependsOn: ['credential'],
@@ -218,7 +216,6 @@ export const LinearBlock: BlockConfig<LinearResponse> = {
title: 'Project',
type: 'project-selector',
canonicalParamId: 'projectId',
provider: 'linear',
serviceId: 'linear',
placeholder: 'Select a project',
dependsOn: ['credential', 'teamId'],

View File

@@ -32,7 +32,6 @@ export const LinkedInBlock: BlockConfig<LinkedInResponse> = {
id: 'credential',
title: 'LinkedIn Account',
type: 'oauth-input',
provider: 'linkedin',
serviceId: 'linkedin',
requiredScopes: ['profile', 'openid', 'email', 'w_member_social'],
placeholder: 'Select LinkedIn account',

View File

@@ -31,7 +31,6 @@ export const MicrosoftExcelBlock: BlockConfig<MicrosoftExcelResponse> = {
id: 'credential',
title: 'Microsoft Account',
type: 'oauth-input',
provider: 'microsoft-excel',
serviceId: 'microsoft-excel',
requiredScopes: [
'openid',
@@ -49,7 +48,6 @@ export const MicrosoftExcelBlock: BlockConfig<MicrosoftExcelResponse> = {
title: 'Select Sheet',
type: 'file-selector',
canonicalParamId: 'spreadsheetId',
provider: 'microsoft-excel',
serviceId: 'microsoft-excel',
requiredScopes: [],
mimeType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',

View File

@@ -61,7 +61,6 @@ export const MicrosoftPlannerBlock: BlockConfig<MicrosoftPlannerResponse> = {
id: 'credential',
title: 'Microsoft Account',
type: 'oauth-input',
provider: 'microsoft-planner',
serviceId: 'microsoft-planner',
requiredScopes: [
'openid',
@@ -94,7 +93,7 @@ export const MicrosoftPlannerBlock: BlockConfig<MicrosoftPlannerResponse> = {
title: 'Task ID',
type: 'file-selector',
placeholder: 'Select a task',
provider: 'microsoft-planner',
serviceId: 'microsoft-planner',
condition: { field: 'operation', value: ['read_task'] },
dependsOn: ['credential', 'planId'],
mode: 'basic',

View File

@@ -43,7 +43,6 @@ export const MicrosoftTeamsBlock: BlockConfig<MicrosoftTeamsResponse> = {
id: 'credential',
title: 'Microsoft Account',
type: 'oauth-input',
provider: 'microsoft-teams',
serviceId: 'microsoft-teams',
requiredScopes: [
'openid',
@@ -75,7 +74,6 @@ export const MicrosoftTeamsBlock: BlockConfig<MicrosoftTeamsResponse> = {
title: 'Select Team',
type: 'file-selector',
canonicalParamId: 'teamId',
provider: 'microsoft-teams',
serviceId: 'microsoft-teams',
requiredScopes: [],
placeholder: 'Select a team',
@@ -119,7 +117,6 @@ export const MicrosoftTeamsBlock: BlockConfig<MicrosoftTeamsResponse> = {
title: 'Select Chat',
type: 'file-selector',
canonicalParamId: 'chatId',
provider: 'microsoft-teams',
serviceId: 'microsoft-teams',
requiredScopes: [],
placeholder: 'Select a chat',
@@ -147,7 +144,6 @@ export const MicrosoftTeamsBlock: BlockConfig<MicrosoftTeamsResponse> = {
title: 'Select Channel',
type: 'file-selector',
canonicalParamId: 'channelId',
provider: 'microsoft-teams',
serviceId: 'microsoft-teams',
requiredScopes: [],
placeholder: 'Select a channel',

View File

@@ -34,7 +34,6 @@ export const NotionBlock: BlockConfig<NotionResponse> = {
id: 'credential',
title: 'Notion Account',
type: 'oauth-input',
provider: 'notion',
serviceId: 'notion',
requiredScopes: ['workspace.content', 'workspace.name', 'page.read', 'page.write'],
placeholder: 'Select Notion account',

View File

@@ -38,7 +38,6 @@ export const OneDriveBlock: BlockConfig<OneDriveResponse> = {
id: 'credential',
title: 'Microsoft Account',
type: 'oauth-input',
provider: 'onedrive',
serviceId: 'onedrive',
requiredScopes: [
'openid',
@@ -144,7 +143,6 @@ export const OneDriveBlock: BlockConfig<OneDriveResponse> = {
title: 'Select Parent Folder',
type: 'file-selector',
canonicalParamId: 'folderId',
provider: 'microsoft',
serviceId: 'onedrive',
requiredScopes: [
'openid',
@@ -182,7 +180,6 @@ export const OneDriveBlock: BlockConfig<OneDriveResponse> = {
title: 'Select Parent Folder',
type: 'file-selector',
canonicalParamId: 'folderId',
provider: 'microsoft',
serviceId: 'onedrive',
requiredScopes: [
'openid',
@@ -215,7 +212,6 @@ export const OneDriveBlock: BlockConfig<OneDriveResponse> = {
title: 'Select Folder',
type: 'file-selector',
canonicalParamId: 'folderId',
provider: 'microsoft',
serviceId: 'onedrive',
requiredScopes: [
'openid',
@@ -262,7 +258,6 @@ export const OneDriveBlock: BlockConfig<OneDriveResponse> = {
title: 'Select File',
type: 'file-selector',
canonicalParamId: 'fileId',
provider: 'microsoft',
serviceId: 'onedrive',
requiredScopes: [
'openid',
@@ -302,7 +297,6 @@ export const OneDriveBlock: BlockConfig<OneDriveResponse> = {
title: 'Select File to Delete',
type: 'file-selector',
canonicalParamId: 'fileId',
provider: 'microsoft',
serviceId: 'onedrive',
requiredScopes: [
'openid',

View File

@@ -38,7 +38,6 @@ export const OutlookBlock: BlockConfig<OutlookResponse> = {
id: 'credential',
title: 'Microsoft Account',
type: 'oauth-input',
provider: 'outlook',
serviceId: 'outlook',
requiredScopes: [
'Mail.ReadWrite',
@@ -175,7 +174,6 @@ export const OutlookBlock: BlockConfig<OutlookResponse> = {
title: 'Folder',
type: 'folder-selector',
canonicalParamId: 'folder',
provider: 'outlook',
serviceId: 'outlook',
requiredScopes: ['Mail.ReadWrite', 'Mail.ReadBasic', 'Mail.Read'],
placeholder: 'Select Outlook folder',
@@ -221,7 +219,6 @@ export const OutlookBlock: BlockConfig<OutlookResponse> = {
title: 'Move To Folder',
type: 'folder-selector',
canonicalParamId: 'destinationId',
provider: 'outlook',
serviceId: 'outlook',
requiredScopes: ['Mail.ReadWrite', 'Mail.ReadBasic', 'Mail.Read'],
placeholder: 'Select destination folder',
@@ -268,7 +265,6 @@ export const OutlookBlock: BlockConfig<OutlookResponse> = {
title: 'Copy To Folder',
type: 'folder-selector',
canonicalParamId: 'copyDestinationId',
provider: 'outlook',
serviceId: 'outlook',
requiredScopes: ['Mail.ReadWrite', 'Mail.ReadBasic', 'Mail.Read'],
placeholder: 'Select destination folder',

View File

@@ -45,7 +45,6 @@ export const PipedriveBlock: BlockConfig<PipedriveResponse> = {
id: 'credential',
title: 'Pipedrive Account',
type: 'oauth-input',
provider: 'pipedrive',
serviceId: 'pipedrive',
requiredScopes: [
'base',

View File

@@ -0,0 +1,326 @@
import { RDSIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types'
import type { RdsResponse } from '@/tools/rds/types'
export const RDSBlock: BlockConfig<RdsResponse> = {
type: 'rds',
name: 'Amazon RDS',
description: 'Connect to Amazon RDS via Data API',
longDescription:
'Integrate Amazon RDS Aurora Serverless into the workflow using the Data API. Can query, insert, update, delete, and execute raw SQL without managing database connections.',
docsLink: 'https://docs.sim.ai/tools/rds',
category: 'tools',
bgColor: 'linear-gradient(45deg, #2E27AD 0%, #527FFF 100%)',
icon: RDSIcon,
subBlocks: [
{
id: 'operation',
title: 'Operation',
type: 'dropdown',
options: [
{ label: 'Query (SELECT)', id: 'query' },
{ label: 'Insert Data', id: 'insert' },
{ label: 'Update Data', id: 'update' },
{ label: 'Delete Data', id: 'delete' },
{ label: 'Execute Raw SQL', id: 'execute' },
],
value: () => 'query',
},
{
id: 'region',
title: 'AWS Region',
type: 'short-input',
placeholder: 'us-east-1',
required: true,
},
{
id: 'accessKeyId',
title: 'AWS Access Key ID',
type: 'short-input',
placeholder: 'AKIA...',
password: true,
required: true,
},
{
id: 'secretAccessKey',
title: 'AWS Secret Access Key',
type: 'short-input',
placeholder: 'Your secret access key',
password: true,
required: true,
},
{
id: 'resourceArn',
title: 'Resource ARN',
type: 'short-input',
placeholder: 'arn:aws:rds:us-east-1:123456789:cluster:my-cluster',
required: true,
},
{
id: 'secretArn',
title: 'Secret ARN',
type: 'short-input',
placeholder: 'arn:aws:secretsmanager:us-east-1:123456789:secret:my-secret',
required: true,
},
{
id: 'database',
title: 'Database Name',
type: 'short-input',
placeholder: 'your_database',
required: false,
},
// Table field for insert/update/delete operations
{
id: 'table',
title: 'Table Name',
type: 'short-input',
placeholder: 'users',
condition: { field: 'operation', value: 'insert' },
required: true,
},
{
id: 'table',
title: 'Table Name',
type: 'short-input',
placeholder: 'users',
condition: { field: 'operation', value: 'update' },
required: true,
},
{
id: 'table',
title: 'Table Name',
type: 'short-input',
placeholder: 'users',
condition: { field: 'operation', value: 'delete' },
required: true,
},
// SQL Query field
{
id: 'query',
title: 'SQL Query',
type: 'code',
placeholder: 'SELECT * FROM users WHERE active = true',
condition: { field: 'operation', value: 'query' },
required: true,
wandConfig: {
enabled: true,
maintainHistory: true,
prompt: `You are an expert SQL database developer. Write SQL queries based on the user's request.
### CONTEXT
{context}
### CRITICAL INSTRUCTION
Return ONLY the SQL query. Do not include any explanations, markdown formatting, comments, or additional text. Just the raw SQL query.
### QUERY GUIDELINES
1. **Syntax**: Use standard SQL syntax compatible with both MySQL and PostgreSQL
2. **Performance**: Write efficient queries with proper indexing considerations
3. **Security**: Use parameterized queries when applicable
4. **Readability**: Format queries with proper indentation and spacing
5. **Best Practices**: Follow standard SQL naming conventions
### EXAMPLES
**Simple Select**: "Get all active users"
→ SELECT id, name, email, created_at
FROM users
WHERE active = true
ORDER BY created_at DESC;
**Complex Join**: "Get users with their order counts and total spent"
→ SELECT
u.id,
u.name,
u.email,
COUNT(o.id) as order_count,
COALESCE(SUM(o.total), 0) as total_spent
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.active = true
GROUP BY u.id, u.name, u.email
HAVING COUNT(o.id) > 0
ORDER BY total_spent DESC;
### REMEMBER
Return ONLY the SQL query - no explanations, no markdown, no extra text.`,
placeholder: 'Describe the SQL query you need...',
generationType: 'sql-query',
},
},
{
id: 'query',
title: 'SQL Query',
type: 'code',
placeholder: 'SELECT * FROM table_name',
condition: { field: 'operation', value: 'execute' },
required: true,
wandConfig: {
enabled: true,
maintainHistory: true,
prompt: `You are an expert SQL database developer. Write SQL queries based on the user's request.
### CONTEXT
{context}
### CRITICAL INSTRUCTION
Return ONLY the SQL query. Do not include any explanations, markdown formatting, comments, or additional text. Just the raw SQL query.
### QUERY GUIDELINES
1. **Syntax**: Use standard SQL syntax compatible with both MySQL and PostgreSQL
2. **Performance**: Write efficient queries with proper indexing considerations
3. **Security**: Use parameterized queries when applicable
4. **Readability**: Format queries with proper indentation and spacing
5. **Best Practices**: Follow standard SQL naming conventions
### EXAMPLES
**Simple Select**: "Get all active users"
→ SELECT id, name, email, created_at
FROM users
WHERE active = true
ORDER BY created_at DESC;
**Create Table**: "Create a users table"
→ CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(255) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
active BOOLEAN DEFAULT true,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
### REMEMBER
Return ONLY the SQL query - no explanations, no markdown, no extra text.`,
placeholder: 'Describe the SQL query you need...',
generationType: 'sql-query',
},
},
// Data for insert operations
{
id: 'data',
title: 'Data (JSON)',
type: 'code',
placeholder: '{\n "name": "John Doe",\n "email": "john@example.com",\n "active": true\n}',
condition: { field: 'operation', value: 'insert' },
required: true,
},
// Set clause for updates
{
id: 'data',
title: 'Update Data (JSON)',
type: 'code',
placeholder: '{\n "name": "Jane Doe",\n "email": "jane@example.com"\n}',
condition: { field: 'operation', value: 'update' },
required: true,
},
// Conditions for update/delete (parameterized for SQL injection prevention)
{
id: 'conditions',
title: 'Conditions (JSON)',
type: 'code',
placeholder: '{\n "id": 1\n}',
condition: { field: 'operation', value: 'update' },
required: true,
},
{
id: 'conditions',
title: 'Conditions (JSON)',
type: 'code',
placeholder: '{\n "id": 1\n}',
condition: { field: 'operation', value: 'delete' },
required: true,
},
],
tools: {
access: ['rds_query', 'rds_insert', 'rds_update', 'rds_delete', 'rds_execute'],
config: {
tool: (params) => {
switch (params.operation) {
case 'query':
return 'rds_query'
case 'insert':
return 'rds_insert'
case 'update':
return 'rds_update'
case 'delete':
return 'rds_delete'
case 'execute':
return 'rds_execute'
default:
throw new Error(`Invalid RDS operation: ${params.operation}`)
}
},
params: (params) => {
const { operation, data, conditions, ...rest } = params
// Parse JSON fields
const parseJson = (value: unknown, fieldName: string) => {
if (!value) return undefined
if (typeof value === 'object') return value
if (typeof value === 'string' && value.trim()) {
try {
return JSON.parse(value)
} catch (parseError) {
const errorMsg =
parseError instanceof Error ? parseError.message : 'Unknown JSON error'
throw new Error(`Invalid JSON in ${fieldName}: ${errorMsg}`)
}
}
return undefined
}
const parsedData = parseJson(data, 'data')
const parsedConditions = parseJson(conditions, 'conditions')
// Build connection config
const connectionConfig = {
region: rest.region,
accessKeyId: rest.accessKeyId,
secretAccessKey: rest.secretAccessKey,
resourceArn: rest.resourceArn,
secretArn: rest.secretArn,
database: rest.database,
}
// Build params object
const result: Record<string, unknown> = { ...connectionConfig }
if (rest.table) result.table = rest.table
if (rest.query) result.query = rest.query
if (parsedConditions !== undefined) result.conditions = parsedConditions
if (parsedData !== undefined) result.data = parsedData
return result
},
},
},
inputs: {
operation: { type: 'string', description: 'Database operation to perform' },
region: { type: 'string', description: 'AWS region' },
accessKeyId: { type: 'string', description: 'AWS access key ID' },
secretAccessKey: { type: 'string', description: 'AWS secret access key' },
resourceArn: { type: 'string', description: 'Aurora DB cluster ARN' },
secretArn: { type: 'string', description: 'Secrets Manager secret ARN' },
database: { type: 'string', description: 'Database name' },
table: { type: 'string', description: 'Table name' },
query: { type: 'string', description: 'SQL query to execute' },
data: { type: 'json', description: 'Data for insert/update operations' },
conditions: { type: 'json', description: 'Conditions for update/delete (e.g., {"id": 1})' },
},
outputs: {
message: {
type: 'string',
description: 'Success or error message describing the operation outcome',
},
rows: {
type: 'array',
description: 'Array of rows returned from the query',
},
rowCount: {
type: 'number',
description: 'Number of rows affected by the operation',
},
},
}

View File

@@ -42,7 +42,6 @@ export const RedditBlock: BlockConfig<RedditResponse> = {
id: 'credential',
title: 'Reddit Account',
type: 'oauth-input',
provider: 'reddit',
serviceId: 'reddit',
requiredScopes: [
'identity',

View File

@@ -12,7 +12,7 @@ export const S3Block: BlockConfig<S3Response> = {
'Integrate S3 into the workflow. Upload files, download objects, list bucket contents, delete objects, and copy objects between buckets. Requires AWS access key and secret access key.',
docsLink: 'https://docs.sim.ai/tools/s3',
category: 'tools',
bgColor: '#E0E0E0',
bgColor: 'linear-gradient(45deg, #1B660F 0%, #6CAE3E 100%)',
icon: S3Icon,
subBlocks: [
// Operation selector

View File

@@ -51,7 +51,6 @@ export const SalesforceBlock: BlockConfig<SalesforceResponse> = {
id: 'credential',
title: 'Salesforce Account',
type: 'oauth-input',
provider: 'salesforce',
serviceId: 'salesforce',
requiredScopes: ['api', 'refresh_token', 'openid'],
placeholder: 'Select Salesforce account',

View File

@@ -37,7 +37,6 @@ export const SharepointBlock: BlockConfig<SharepointResponse> = {
id: 'credential',
title: 'Microsoft Account',
type: 'oauth-input',
provider: 'sharepoint',
serviceId: 'sharepoint',
requiredScopes: [
'openid',
@@ -56,7 +55,6 @@ export const SharepointBlock: BlockConfig<SharepointResponse> = {
title: 'Select Site',
type: 'file-selector',
canonicalParamId: 'siteId',
provider: 'microsoft',
serviceId: 'sharepoint',
requiredScopes: [
'openid',

View File

@@ -48,7 +48,6 @@ export const SlackBlock: BlockConfig<SlackResponse> = {
id: 'credential',
title: 'Slack Account',
type: 'oauth-input',
provider: 'slack',
serviceId: 'slack',
requiredScopes: [
'channels:read',
@@ -85,7 +84,7 @@ export const SlackBlock: BlockConfig<SlackResponse> = {
title: 'Channel',
type: 'channel-selector',
canonicalParamId: 'channel',
provider: 'slack',
serviceId: 'slack',
placeholder: 'Select Slack channel',
mode: 'basic',
dependsOn: ['credential', 'authMethod'],

View File

@@ -41,7 +41,6 @@ export const TrelloBlock: BlockConfig<ToolResponse> = {
id: 'credential',
title: 'Trello Account',
type: 'oauth-input',
provider: 'trello',
serviceId: 'trello',
requiredScopes: ['read', 'write'],
placeholder: 'Select Trello account',

View File

@@ -33,7 +33,6 @@ export const WealthboxBlock: BlockConfig<WealthboxResponse> = {
id: 'credential',
title: 'Wealthbox Account',
type: 'oauth-input',
provider: 'wealthbox',
serviceId: 'wealthbox',
requiredScopes: ['login', 'data'],
placeholder: 'Select Wealthbox account',
@@ -50,7 +49,6 @@ export const WealthboxBlock: BlockConfig<WealthboxResponse> = {
id: 'contactId',
title: 'Select Contact',
type: 'file-selector',
provider: 'wealthbox',
serviceId: 'wealthbox',
requiredScopes: ['login', 'data'],
placeholder: 'Enter Contact ID',

View File

@@ -34,7 +34,6 @@ export const WebflowBlock: BlockConfig<WebflowResponse> = {
id: 'credential',
title: 'Webflow Account',
type: 'oauth-input',
provider: 'webflow',
serviceId: 'webflow',
requiredScopes: ['sites:read', 'sites:write', 'cms:read', 'cms:write'],
placeholder: 'Select Webflow account',

View File

@@ -90,7 +90,6 @@ export const WebhookBlock: BlockConfig = {
id: 'gmailCredential',
title: 'Gmail Account',
type: 'oauth-input',
provider: 'google-email',
serviceId: 'gmail',
requiredScopes: [
'https://www.googleapis.com/auth/gmail.modify',
@@ -104,7 +103,6 @@ export const WebhookBlock: BlockConfig = {
id: 'outlookCredential',
title: 'Microsoft Account',
type: 'oauth-input',
provider: 'outlook',
serviceId: 'outlook',
requiredScopes: [
'Mail.ReadWrite',

View File

@@ -31,7 +31,6 @@ export const XBlock: BlockConfig<XResponse> = {
id: 'credential',
title: 'X Account',
type: 'oauth-input',
provider: 'x',
serviceId: 'x',
requiredScopes: ['tweet.read', 'tweet.write', 'users.read', 'offline.access'],
placeholder: 'Select X account',

View File

@@ -13,6 +13,7 @@ import { ClayBlock } from '@/blocks/blocks/clay'
import { ConditionBlock } from '@/blocks/blocks/condition'
import { ConfluenceBlock } from '@/blocks/blocks/confluence'
import { DiscordBlock } from '@/blocks/blocks/discord'
import { DynamoDBBlock } from '@/blocks/blocks/dynamodb'
import { ElevenLabsBlock } from '@/blocks/blocks/elevenlabs'
import { EvaluatorBlock } from '@/blocks/blocks/evaluator'
import { ExaBlock } from '@/blocks/blocks/exa'
@@ -70,6 +71,7 @@ import { PostgreSQLBlock } from '@/blocks/blocks/postgresql'
import { PostHogBlock } from '@/blocks/blocks/posthog'
import { PylonBlock } from '@/blocks/blocks/pylon'
import { QdrantBlock } from '@/blocks/blocks/qdrant'
import { RDSBlock } from '@/blocks/blocks/rds'
import { RedditBlock } from '@/blocks/blocks/reddit'
import { ResendBlock } from '@/blocks/blocks/resend'
import { ResponseBlock } from '@/blocks/blocks/response'
@@ -189,6 +191,8 @@ export const registry: Record<string, BlockConfig> = {
posthog: PostHogBlock,
pylon: PylonBlock,
qdrant: QdrantBlock,
rds: RDSBlock,
dynamodb: DynamoDBBlock,
reddit: RedditBlock,
resend: ResendBlock,
response: ResponseBlock,

View File

@@ -222,8 +222,7 @@ export interface SubBlockConfig {
generationType?: GenerationType
collapsible?: boolean // Whether the code block can be collapsed
defaultCollapsed?: boolean // Whether the code block is collapsed by default
// OAuth specific properties
provider?: string
// OAuth specific properties - serviceId is the canonical identifier for OAuth services
serviceId?: string
requiredScopes?: string[]
// File selector specific properties

File diff suppressed because one or more lines are too long

View File

@@ -138,6 +138,52 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
}))
},
},
'microsoft.chats': {
key: 'microsoft.chats',
staleTime: SELECTOR_STALE,
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'microsoft.chats',
context.credentialId ?? 'none',
],
enabled: ({ context }) => Boolean(context.credentialId),
fetchList: async ({ context }: SelectorQueryArgs) => {
const body = JSON.stringify({ credential: context.credentialId })
const data = await fetchJson<{ chats: { id: string; displayName: string }[] }>(
'/api/tools/microsoft-teams/chats',
{ method: 'POST', body }
)
return (data.chats || []).map((chat) => ({
id: chat.id,
label: chat.displayName,
}))
},
},
'microsoft.channels': {
key: 'microsoft.channels',
staleTime: SELECTOR_STALE,
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'microsoft.channels',
context.credentialId ?? 'none',
context.teamId ?? 'none',
],
enabled: ({ context }) => Boolean(context.credentialId && context.teamId),
fetchList: async ({ context }: SelectorQueryArgs) => {
const body = JSON.stringify({
credential: context.credentialId,
teamId: context.teamId,
})
const data = await fetchJson<{ channels: { id: string; displayName: string }[] }>(
'/api/tools/microsoft-teams/channels',
{ method: 'POST', body }
)
return (data.channels || []).map((channel) => ({
id: channel.id,
label: channel.displayName,
}))
},
},
'wealthbox.contacts': {
key: 'wealthbox.contacts',
staleTime: SELECTOR_STALE,

View File

@@ -64,9 +64,10 @@ function resolveFileSelector(
mimeType: subBlock.mimeType,
})
const provider = subBlock.provider || subBlock.serviceId || ''
// Use serviceId as the canonical identifier
const serviceId = subBlock.serviceId || ''
switch (provider) {
switch (serviceId) {
case 'google-calendar':
return { key: 'google.calendar', context, allowSearch: false }
case 'confluence':
@@ -74,7 +75,15 @@ function resolveFileSelector(
case 'jira':
return { key: 'jira.issues', context, allowSearch: true }
case 'microsoft-teams':
return { key: 'microsoft.teams', context, allowSearch: true }
// Route to the correct selector based on what type of resource is being selected
if (subBlock.id === 'chatId') {
return { key: 'microsoft.chats', context, allowSearch: false }
}
if (subBlock.id === 'channelId') {
return { key: 'microsoft.channels', context, allowSearch: false }
}
// Default to teams selector for teamId
return { key: 'microsoft.teams', context, allowSearch: false }
case 'wealthbox':
return { key: 'wealthbox.contacts', context, allowSearch: true }
case 'microsoft-planner':
@@ -89,36 +98,33 @@ function resolveFileSelector(
return { key: 'google.drive', context, allowSearch: true }
case 'google-docs':
return { key: 'google.drive', context, allowSearch: true }
case 'onedrive': {
const key: SelectorKey = subBlock.mimeType === 'file' ? 'onedrive.files' : 'onedrive.folders'
return { key, context, allowSearch: true }
}
case 'sharepoint':
return { key: 'sharepoint.sites', context, allowSearch: true }
default:
break
return { key: null, context, allowSearch: true }
}
if (subBlock.serviceId === 'onedrive') {
const key: SelectorKey = subBlock.mimeType === 'file' ? 'onedrive.files' : 'onedrive.folders'
return { key, context, allowSearch: true }
}
if (subBlock.serviceId === 'sharepoint') {
return { key: 'sharepoint.sites', context, allowSearch: true }
}
if (subBlock.serviceId === 'google-sheets') {
return { key: 'google.drive', context, allowSearch: true }
}
return { key: null, context, allowSearch: true }
}
function resolveFolderSelector(
subBlock: SubBlockConfig,
args: SelectorResolutionArgs
): SelectorResolution {
const provider = (subBlock.provider || subBlock.serviceId || 'gmail').toLowerCase()
const key: SelectorKey = provider === 'outlook' ? 'outlook.folders' : 'gmail.labels'
return {
key,
context: buildBaseContext(args),
allowSearch: true,
const serviceId = subBlock.serviceId?.toLowerCase()
if (!serviceId) {
return { key: null, context: buildBaseContext(args), allowSearch: true }
}
switch (serviceId) {
case 'gmail':
return { key: 'gmail.labels', context: buildBaseContext(args), allowSearch: true }
case 'outlook':
return { key: 'outlook.folders', context: buildBaseContext(args), allowSearch: true }
default:
return { key: null, context: buildBaseContext(args), allowSearch: true }
}
}
@@ -126,8 +132,8 @@ function resolveChannelSelector(
subBlock: SubBlockConfig,
args: SelectorResolutionArgs
): SelectorResolution {
const provider = subBlock.provider || 'slack'
if (provider !== 'slack') {
const serviceId = subBlock.serviceId
if (serviceId !== 'slack') {
return { key: null, context: buildBaseContext(args), allowSearch: true }
}
return {
@@ -141,22 +147,18 @@ function resolveProjectSelector(
subBlock: SubBlockConfig,
args: SelectorResolutionArgs
): SelectorResolution {
const provider = subBlock.provider || 'jira'
const serviceId = subBlock.serviceId
const context = buildBaseContext(args)
if (provider === 'linear') {
const key: SelectorKey = subBlock.id === 'teamId' ? 'linear.teams' : 'linear.projects'
return {
key,
context,
allowSearch: true,
switch (serviceId) {
case 'linear': {
const key: SelectorKey = subBlock.id === 'teamId' ? 'linear.teams' : 'linear.projects'
return { key, context, allowSearch: true }
}
}
return {
key: 'jira.projects',
context,
allowSearch: true,
case 'jira':
return { key: 'jira.projects', context, allowSearch: true }
default:
return { key: null, context, allowSearch: true }
}
}

View File

@@ -12,6 +12,8 @@ export type SelectorKey =
| 'linear.teams'
| 'confluence.pages'
| 'microsoft.teams'
| 'microsoft.chats'
| 'microsoft.channels'
| 'wealthbox.contacts'
| 'onedrive.files'
| 'onedrive.folders'
@@ -33,7 +35,6 @@ export interface SelectorContext {
workspaceId?: string
workflowId?: string
credentialId?: string
provider?: string
serviceId?: string
domain?: string
teamId?: string

View File

@@ -39,7 +39,6 @@ export interface CopilotSubblockMetadata {
language?: string
generationType?: string
// OAuth/credential properties
provider?: string
serviceId?: string
requiredScopes?: string[]
// File properties
@@ -627,7 +626,6 @@ function processSubBlock(sb: any): CopilotSubblockMetadata {
generationType: sb.generationType,
// OAuth/credential properties
provider: sb.provider,
serviceId: sb.serviceId,
requiredScopes: sb.requiredScopes,

View File

@@ -11,7 +11,7 @@ export enum CredentialType {
// Type for credential requirement
export interface CredentialRequirement {
type: CredentialType
provider?: string // For OAuth (e.g., 'google-drive', 'slack')
serviceId?: string // For OAuth (e.g., 'google-drive', 'slack')
label: string // Human-readable label
blockType: string // The block type that requires this
subBlockId: string // The subblock ID for reference
@@ -72,7 +72,7 @@ export function extractRequiredCredentials(state: any): CredentialRequirement[]
seen.add(key)
credentials.push({
type: CredentialType.OAUTH,
provider: block.type,
serviceId: block.type,
label: `Credential for ${blockName}`,
blockType: block.type,
subBlockId: 'oauth',

View File

@@ -1,5 +1,5 @@
import { createLogger } from '@/lib/logs/console/logger'
import { getProviderIdFromServiceId, getServiceIdFromScopes } from '@/lib/oauth/oauth'
import { getProviderIdFromServiceId } from '@/lib/oauth/oauth'
import { getBlock } from '@/blocks/index'
import type { SubBlockConfig } from '@/blocks/types'
import type { BlockState } from '@/stores/workflows/workflow/types'
@@ -52,7 +52,7 @@ export async function resolveCredentialsForWorkflow(
logger.debug(`Checking credential for ${blockId}.${subBlockId}`, {
blockType: blockState.type,
provider: subBlockConfig.provider,
serviceId: subBlockConfig.serviceId,
hasExistingValue: !!existingValue,
existingValue,
})
@@ -70,13 +70,13 @@ export async function resolveCredentialsForWorkflow(
resolvedValues[blockId][subBlockId] = credentialId
logger.info(`Auto-selected credential for ${blockId}.${subBlockId}`, {
blockType: blockState.type,
provider: subBlockConfig.provider,
serviceId: subBlockConfig.serviceId,
credentialId,
})
} else {
logger.info(`No credential auto-selected for ${blockId}.${subBlockId}`, {
blockType: blockState.type,
provider: subBlockConfig.provider,
serviceId: subBlockConfig.serviceId,
})
}
}
@@ -102,7 +102,6 @@ export async function resolveCredentialsForWorkflow(
*/
async function resolveCredentialForSubBlock(
subBlockConfig: SubBlockConfig & {
provider?: string
requiredScopes?: string[]
serviceId?: string
},
@@ -110,29 +109,26 @@ async function resolveCredentialForSubBlock(
userId?: string
): Promise<string | null> {
try {
const provider = subBlockConfig.provider
const requiredScopes = subBlockConfig.requiredScopes || []
const serviceId = subBlockConfig.serviceId
logger.debug('Resolving credential for subblock', {
blockType: blockState.type,
provider,
serviceId,
requiredScopes,
userId,
})
if (!provider) {
logger.debug('No provider specified, skipping credential resolution')
if (!serviceId) {
logger.debug('No serviceId specified, skipping credential resolution')
return null
}
// Derive service and provider IDs
const effectiveServiceId = serviceId || getServiceIdFromScopes(provider as any, requiredScopes)
const effectiveProviderId = getProviderIdFromServiceId(effectiveServiceId)
// Derive providerId from serviceId using OAuth config
const effectiveProviderId = getProviderIdFromServiceId(serviceId)
logger.debug('Derived provider info', {
effectiveServiceId,
serviceId,
effectiveProviderId,
})

View File

@@ -24,7 +24,10 @@
},
"dependencies": {
"@anthropic-ai/sdk": "^0.39.0",
"@aws-sdk/client-dynamodb": "3.940.0",
"@aws-sdk/client-rds-data": "3.940.0",
"@aws-sdk/client-s3": "^3.779.0",
"@aws-sdk/lib-dynamodb": "3.940.0",
"@aws-sdk/s3-request-presigner": "^3.779.0",
"@azure/communication-email": "1.0.0",
"@azure/storage-blob": "12.27.0",

View File

@@ -0,0 +1,84 @@
import type { DynamoDBDeleteParams, DynamoDBDeleteResponse } from '@/tools/dynamodb/types'
import type { ToolConfig } from '@/tools/types'
export const deleteTool: ToolConfig<DynamoDBDeleteParams, DynamoDBDeleteResponse> = {
id: 'dynamodb_delete',
name: 'DynamoDB Delete',
description: 'Delete an item from a DynamoDB table',
version: '1.0',
params: {
region: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'AWS region (e.g., us-east-1)',
},
accessKeyId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'AWS access key ID',
},
secretAccessKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'AWS secret access key',
},
tableName: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'DynamoDB table name',
},
key: {
type: 'object',
required: true,
visibility: 'user-or-llm',
description: 'Primary key of the item to delete',
},
conditionExpression: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Condition that must be met for the delete to succeed',
},
},
request: {
url: '/api/tools/dynamodb/delete',
method: 'POST',
headers: () => ({
'Content-Type': 'application/json',
}),
body: (params) => ({
region: params.region,
accessKeyId: params.accessKeyId,
secretAccessKey: params.secretAccessKey,
tableName: params.tableName,
key: params.key,
...(params.conditionExpression && { conditionExpression: params.conditionExpression }),
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(data.error || 'DynamoDB delete failed')
}
return {
success: true,
output: {
message: data.message || 'Item deleted successfully',
},
error: undefined,
}
},
outputs: {
message: { type: 'string', description: 'Operation status message' },
},
}

View File

@@ -0,0 +1,86 @@
import type { DynamoDBGetParams, DynamoDBGetResponse } from '@/tools/dynamodb/types'
import type { ToolConfig } from '@/tools/types'
export const getTool: ToolConfig<DynamoDBGetParams, DynamoDBGetResponse> = {
id: 'dynamodb_get',
name: 'DynamoDB Get',
description: 'Get an item from a DynamoDB table by primary key',
version: '1.0',
params: {
region: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'AWS region (e.g., us-east-1)',
},
accessKeyId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'AWS access key ID',
},
secretAccessKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'AWS secret access key',
},
tableName: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'DynamoDB table name',
},
key: {
type: 'object',
required: true,
visibility: 'user-or-llm',
description: 'Primary key of the item to retrieve',
},
consistentRead: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Use strongly consistent read',
},
},
request: {
url: '/api/tools/dynamodb/get',
method: 'POST',
headers: () => ({
'Content-Type': 'application/json',
}),
body: (params) => ({
region: params.region,
accessKeyId: params.accessKeyId,
secretAccessKey: params.secretAccessKey,
tableName: params.tableName,
key: params.key,
...(params.consistentRead !== undefined && { consistentRead: params.consistentRead }),
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(data.error || 'DynamoDB get failed')
}
return {
success: true,
output: {
message: data.message || 'Item retrieved successfully',
item: data.item,
},
error: undefined,
}
},
outputs: {
message: { type: 'string', description: 'Operation status message' },
item: { type: 'object', description: 'Retrieved item' },
},
}

View File

@@ -0,0 +1,8 @@
import { deleteTool } from './delete'
import { getTool } from './get'
import { putTool } from './put'
import { queryTool } from './query'
import { scanTool } from './scan'
import { updateTool } from './update'
export { deleteTool, getTool, putTool, queryTool, scanTool, updateTool }

View File

@@ -0,0 +1,79 @@
import type { DynamoDBPutParams, DynamoDBPutResponse } from '@/tools/dynamodb/types'
import type { ToolConfig } from '@/tools/types'
export const putTool: ToolConfig<DynamoDBPutParams, DynamoDBPutResponse> = {
id: 'dynamodb_put',
name: 'DynamoDB Put',
description: 'Put an item into a DynamoDB table',
version: '1.0',
params: {
region: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'AWS region (e.g., us-east-1)',
},
accessKeyId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'AWS access key ID',
},
secretAccessKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'AWS secret access key',
},
tableName: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'DynamoDB table name',
},
item: {
type: 'object',
required: true,
visibility: 'user-or-llm',
description: 'Item to put into the table',
},
},
request: {
url: '/api/tools/dynamodb/put',
method: 'POST',
headers: () => ({
'Content-Type': 'application/json',
}),
body: (params) => ({
region: params.region,
accessKeyId: params.accessKeyId,
secretAccessKey: params.secretAccessKey,
tableName: params.tableName,
item: params.item,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(data.error || 'DynamoDB put failed')
}
return {
success: true,
output: {
message: data.message || 'Item created successfully',
item: data.item,
},
error: undefined,
}
},
outputs: {
message: { type: 'string', description: 'Operation status message' },
item: { type: 'object', description: 'Created item' },
},
}

View File

@@ -0,0 +1,120 @@
import type { DynamoDBQueryParams, DynamoDBQueryResponse } from '@/tools/dynamodb/types'
import type { ToolConfig } from '@/tools/types'
export const queryTool: ToolConfig<DynamoDBQueryParams, DynamoDBQueryResponse> = {
id: 'dynamodb_query',
name: 'DynamoDB Query',
description: 'Query items from a DynamoDB table using key conditions',
version: '1.0',
params: {
region: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'AWS region (e.g., us-east-1)',
},
accessKeyId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'AWS access key ID',
},
secretAccessKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'AWS secret access key',
},
tableName: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'DynamoDB table name',
},
keyConditionExpression: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Key condition expression (e.g., "pk = :pk")',
},
filterExpression: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Filter expression for results',
},
expressionAttributeNames: {
type: 'object',
required: false,
visibility: 'user-or-llm',
description: 'Attribute name mappings for reserved words',
},
expressionAttributeValues: {
type: 'object',
required: false,
visibility: 'user-or-llm',
description: 'Expression attribute values',
},
indexName: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Secondary index name to query',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of items to return',
},
},
request: {
url: '/api/tools/dynamodb/query',
method: 'POST',
headers: () => ({
'Content-Type': 'application/json',
}),
body: (params) => ({
region: params.region,
accessKeyId: params.accessKeyId,
secretAccessKey: params.secretAccessKey,
tableName: params.tableName,
keyConditionExpression: params.keyConditionExpression,
...(params.filterExpression && { filterExpression: params.filterExpression }),
...(params.expressionAttributeNames && {
expressionAttributeNames: params.expressionAttributeNames,
}),
...(params.expressionAttributeValues && {
expressionAttributeValues: params.expressionAttributeValues,
}),
...(params.indexName && { indexName: params.indexName }),
...(params.limit && { limit: params.limit }),
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(data.error || 'DynamoDB query failed')
}
return {
success: true,
output: {
message: data.message || 'Query executed successfully',
items: data.items || [],
count: data.count || 0,
},
error: undefined,
}
},
outputs: {
message: { type: 'string', description: 'Operation status message' },
items: { type: 'array', description: 'Array of items returned' },
count: { type: 'number', description: 'Number of items returned' },
},
}

View File

@@ -0,0 +1,113 @@
import type { DynamoDBScanParams, DynamoDBScanResponse } from '@/tools/dynamodb/types'
import type { ToolConfig } from '@/tools/types'
export const scanTool: ToolConfig<DynamoDBScanParams, DynamoDBScanResponse> = {
id: 'dynamodb_scan',
name: 'DynamoDB Scan',
description: 'Scan all items in a DynamoDB table',
version: '1.0',
params: {
region: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'AWS region (e.g., us-east-1)',
},
accessKeyId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'AWS access key ID',
},
secretAccessKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'AWS secret access key',
},
tableName: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'DynamoDB table name',
},
filterExpression: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Filter expression for results',
},
projectionExpression: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Attributes to retrieve',
},
expressionAttributeNames: {
type: 'object',
required: false,
visibility: 'user-or-llm',
description: 'Attribute name mappings for reserved words',
},
expressionAttributeValues: {
type: 'object',
required: false,
visibility: 'user-or-llm',
description: 'Expression attribute values',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of items to return',
},
},
request: {
url: '/api/tools/dynamodb/scan',
method: 'POST',
headers: () => ({
'Content-Type': 'application/json',
}),
body: (params) => ({
region: params.region,
accessKeyId: params.accessKeyId,
secretAccessKey: params.secretAccessKey,
tableName: params.tableName,
...(params.filterExpression && { filterExpression: params.filterExpression }),
...(params.projectionExpression && { projectionExpression: params.projectionExpression }),
...(params.expressionAttributeNames && {
expressionAttributeNames: params.expressionAttributeNames,
}),
...(params.expressionAttributeValues && {
expressionAttributeValues: params.expressionAttributeValues,
}),
...(params.limit && { limit: params.limit }),
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(data.error || 'DynamoDB scan failed')
}
return {
success: true,
output: {
message: data.message || 'Scan executed successfully',
items: data.items || [],
count: data.count || 0,
},
error: undefined,
}
},
outputs: {
message: { type: 'string', description: 'Operation status message' },
items: { type: 'array', description: 'Array of items returned' },
count: { type: 'number', description: 'Number of items returned' },
},
}

View File

@@ -0,0 +1,70 @@
import type { ToolResponse } from '@/tools/types'
export interface DynamoDBConnectionConfig {
region: string
accessKeyId: string
secretAccessKey: string
}
export interface DynamoDBGetParams extends DynamoDBConnectionConfig {
tableName: string
key: Record<string, unknown>
consistentRead?: boolean
}
export interface DynamoDBPutParams extends DynamoDBConnectionConfig {
tableName: string
item: Record<string, unknown>
}
export interface DynamoDBQueryParams extends DynamoDBConnectionConfig {
tableName: string
keyConditionExpression: string
filterExpression?: string
expressionAttributeNames?: Record<string, string>
expressionAttributeValues?: Record<string, unknown>
indexName?: string
limit?: number
}
export interface DynamoDBScanParams extends DynamoDBConnectionConfig {
tableName: string
filterExpression?: string
projectionExpression?: string
expressionAttributeNames?: Record<string, string>
expressionAttributeValues?: Record<string, unknown>
limit?: number
}
export interface DynamoDBUpdateParams extends DynamoDBConnectionConfig {
tableName: string
key: Record<string, unknown>
updateExpression: string
expressionAttributeNames?: Record<string, string>
expressionAttributeValues?: Record<string, unknown>
conditionExpression?: string
}
export interface DynamoDBDeleteParams extends DynamoDBConnectionConfig {
tableName: string
key: Record<string, unknown>
conditionExpression?: string
}
export interface DynamoDBBaseResponse extends ToolResponse {
output: {
message: string
item?: Record<string, unknown>
items?: Record<string, unknown>[]
count?: number
}
error?: string
}
export interface DynamoDBGetResponse extends DynamoDBBaseResponse {}
export interface DynamoDBPutResponse extends DynamoDBBaseResponse {}
export interface DynamoDBQueryResponse extends DynamoDBBaseResponse {}
export interface DynamoDBScanResponse extends DynamoDBBaseResponse {}
export interface DynamoDBUpdateResponse extends DynamoDBBaseResponse {}
export interface DynamoDBDeleteResponse extends DynamoDBBaseResponse {}
export interface DynamoDBResponse extends DynamoDBBaseResponse {}

View File

@@ -0,0 +1,111 @@
import type { DynamoDBUpdateParams, DynamoDBUpdateResponse } from '@/tools/dynamodb/types'
import type { ToolConfig } from '@/tools/types'
export const updateTool: ToolConfig<DynamoDBUpdateParams, DynamoDBUpdateResponse> = {
id: 'dynamodb_update',
name: 'DynamoDB Update',
description: 'Update an item in a DynamoDB table',
version: '1.0',
params: {
region: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'AWS region (e.g., us-east-1)',
},
accessKeyId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'AWS access key ID',
},
secretAccessKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'AWS secret access key',
},
tableName: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'DynamoDB table name',
},
key: {
type: 'object',
required: true,
visibility: 'user-or-llm',
description: 'Primary key of the item to update',
},
updateExpression: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Update expression (e.g., "SET #name = :name")',
},
expressionAttributeNames: {
type: 'object',
required: false,
visibility: 'user-or-llm',
description: 'Attribute name mappings for reserved words',
},
expressionAttributeValues: {
type: 'object',
required: false,
visibility: 'user-or-llm',
description: 'Expression attribute values',
},
conditionExpression: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Condition that must be met for the update to succeed',
},
},
request: {
url: '/api/tools/dynamodb/update',
method: 'POST',
headers: () => ({
'Content-Type': 'application/json',
}),
body: (params) => ({
region: params.region,
accessKeyId: params.accessKeyId,
secretAccessKey: params.secretAccessKey,
tableName: params.tableName,
key: params.key,
updateExpression: params.updateExpression,
...(params.expressionAttributeNames && {
expressionAttributeNames: params.expressionAttributeNames,
}),
...(params.expressionAttributeValues && {
expressionAttributeValues: params.expressionAttributeValues,
}),
...(params.conditionExpression && { conditionExpression: params.conditionExpression }),
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(data.error || 'DynamoDB update failed')
}
return {
success: true,
output: {
message: data.message || 'Item updated successfully',
item: data.item,
},
error: undefined,
}
},
outputs: {
message: { type: 'string', description: 'Operation status message' },
item: { type: 'object', description: 'Updated item' },
},
}

View File

@@ -23,7 +23,6 @@ export interface UIComponentConfig {
condition?: ComponentCondition
title?: string
value?: unknown
provider?: string
serviceId?: string
requiredScopes?: string[]
mimeType?: string
@@ -50,7 +49,6 @@ export interface SubBlockConfig {
password?: boolean
condition?: ComponentCondition
value?: unknown
provider?: string
serviceId?: string
requiredScopes?: string[]
mimeType?: string
@@ -277,7 +275,6 @@ export function getToolParametersConfig(
condition: subBlock.condition,
title: subBlock.title,
value: subBlock.value,
provider: subBlock.provider,
serviceId: subBlock.serviceId,
requiredScopes: subBlock.requiredScopes,
mimeType: subBlock.mimeType,

View File

@@ -0,0 +1,102 @@
import type { RdsDeleteParams, RdsDeleteResponse } from '@/tools/rds/types'
import type { ToolConfig } from '@/tools/types'
export const deleteTool: ToolConfig<RdsDeleteParams, RdsDeleteResponse> = {
id: 'rds_delete',
name: 'RDS Delete',
description: 'Delete data from an Amazon RDS table using the Data API',
version: '1.0',
params: {
region: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'AWS region (e.g., us-east-1)',
},
accessKeyId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'AWS access key ID',
},
secretAccessKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'AWS secret access key',
},
resourceArn: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ARN of the Aurora DB cluster',
},
secretArn: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ARN of the Secrets Manager secret containing DB credentials',
},
database: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Database name (optional)',
},
table: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Table name to delete from',
},
conditions: {
type: 'object',
required: true,
visibility: 'user-or-llm',
description: 'Conditions for the delete (e.g., {"id": 1})',
},
},
request: {
url: '/api/tools/rds/delete',
method: 'POST',
headers: () => ({
'Content-Type': 'application/json',
}),
body: (params) => ({
region: params.region,
accessKeyId: params.accessKeyId,
secretAccessKey: params.secretAccessKey,
resourceArn: params.resourceArn,
secretArn: params.secretArn,
...(params.database && { database: params.database }),
table: params.table,
conditions: params.conditions,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(data.error || 'RDS delete failed')
}
return {
success: true,
output: {
message: data.message || 'Delete executed successfully',
rows: data.rows || [],
rowCount: data.rowCount || 0,
},
error: undefined,
}
},
outputs: {
message: { type: 'string', description: 'Operation status message' },
rows: { type: 'array', description: 'Array of deleted rows' },
rowCount: { type: 'number', description: 'Number of rows deleted' },
},
}

View File

@@ -0,0 +1,95 @@
import type { RdsExecuteParams, RdsExecuteResponse } from '@/tools/rds/types'
import type { ToolConfig } from '@/tools/types'
export const executeTool: ToolConfig<RdsExecuteParams, RdsExecuteResponse> = {
id: 'rds_execute',
name: 'RDS Execute',
description: 'Execute raw SQL on Amazon RDS using the Data API',
version: '1.0',
params: {
region: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'AWS region (e.g., us-east-1)',
},
accessKeyId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'AWS access key ID',
},
secretAccessKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'AWS secret access key',
},
resourceArn: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ARN of the Aurora DB cluster',
},
secretArn: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ARN of the Secrets Manager secret containing DB credentials',
},
database: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Database name (optional)',
},
query: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Raw SQL query to execute',
},
},
request: {
url: '/api/tools/rds/execute',
method: 'POST',
headers: () => ({
'Content-Type': 'application/json',
}),
body: (params) => ({
region: params.region,
accessKeyId: params.accessKeyId,
secretAccessKey: params.secretAccessKey,
resourceArn: params.resourceArn,
secretArn: params.secretArn,
...(params.database && { database: params.database }),
query: params.query,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(data.error || 'RDS execute failed')
}
return {
success: true,
output: {
message: data.message || 'Query executed successfully',
rows: data.rows || [],
rowCount: data.rowCount || 0,
},
error: undefined,
}
},
outputs: {
message: { type: 'string', description: 'Operation status message' },
rows: { type: 'array', description: 'Array of rows returned or affected' },
rowCount: { type: 'number', description: 'Number of rows affected' },
},
}

View File

@@ -0,0 +1,7 @@
import { deleteTool } from './delete'
import { executeTool } from './execute'
import { insertTool } from './insert'
import { queryTool } from './query'
import { updateTool } from './update'
export { deleteTool, executeTool, insertTool, queryTool, updateTool }

View File

@@ -0,0 +1,102 @@
import type { RdsInsertParams, RdsInsertResponse } from '@/tools/rds/types'
import type { ToolConfig } from '@/tools/types'
export const insertTool: ToolConfig<RdsInsertParams, RdsInsertResponse> = {
id: 'rds_insert',
name: 'RDS Insert',
description: 'Insert data into an Amazon RDS table using the Data API',
version: '1.0',
params: {
region: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'AWS region (e.g., us-east-1)',
},
accessKeyId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'AWS access key ID',
},
secretAccessKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'AWS secret access key',
},
resourceArn: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ARN of the Aurora DB cluster',
},
secretArn: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ARN of the Secrets Manager secret containing DB credentials',
},
database: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Database name (optional)',
},
table: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Table name to insert into',
},
data: {
type: 'object',
required: true,
visibility: 'user-or-llm',
description: 'Data to insert as key-value pairs',
},
},
request: {
url: '/api/tools/rds/insert',
method: 'POST',
headers: () => ({
'Content-Type': 'application/json',
}),
body: (params) => ({
region: params.region,
accessKeyId: params.accessKeyId,
secretAccessKey: params.secretAccessKey,
resourceArn: params.resourceArn,
secretArn: params.secretArn,
...(params.database && { database: params.database }),
table: params.table,
data: params.data,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(data.error || 'RDS insert failed')
}
return {
success: true,
output: {
message: data.message || 'Insert executed successfully',
rows: data.rows || [],
rowCount: data.rowCount || 0,
},
error: undefined,
}
},
outputs: {
message: { type: 'string', description: 'Operation status message' },
rows: { type: 'array', description: 'Array of inserted rows' },
rowCount: { type: 'number', description: 'Number of rows inserted' },
},
}

View File

@@ -0,0 +1,95 @@
import type { RdsQueryParams, RdsQueryResponse } from '@/tools/rds/types'
import type { ToolConfig } from '@/tools/types'
export const queryTool: ToolConfig<RdsQueryParams, RdsQueryResponse> = {
id: 'rds_query',
name: 'RDS Query',
description: 'Execute a SELECT query on Amazon RDS using the Data API',
version: '1.0',
params: {
region: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'AWS region (e.g., us-east-1)',
},
accessKeyId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'AWS access key ID',
},
secretAccessKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'AWS secret access key',
},
resourceArn: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ARN of the Aurora DB cluster',
},
secretArn: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ARN of the Secrets Manager secret containing DB credentials',
},
database: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Database name (optional)',
},
query: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'SQL SELECT query to execute',
},
},
request: {
url: '/api/tools/rds/query',
method: 'POST',
headers: () => ({
'Content-Type': 'application/json',
}),
body: (params) => ({
region: params.region,
accessKeyId: params.accessKeyId,
secretAccessKey: params.secretAccessKey,
resourceArn: params.resourceArn,
secretArn: params.secretArn,
...(params.database && { database: params.database }),
query: params.query,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(data.error || 'RDS query failed')
}
return {
success: true,
output: {
message: data.message || 'Query executed successfully',
rows: data.rows || [],
rowCount: data.rowCount || 0,
},
error: undefined,
}
},
outputs: {
message: { type: 'string', description: 'Operation status message' },
rows: { type: 'array', description: 'Array of rows returned from the query' },
rowCount: { type: 'number', description: 'Number of rows returned' },
},
}

View File

@@ -0,0 +1,50 @@
import type { ToolResponse } from '@/tools/types'
export interface RdsConnectionConfig {
region: string
accessKeyId: string
secretAccessKey: string
resourceArn: string
secretArn: string
database?: string
}
export interface RdsQueryParams extends RdsConnectionConfig {
query: string
}
export interface RdsInsertParams extends RdsConnectionConfig {
table: string
data: Record<string, unknown>
}
export interface RdsUpdateParams extends RdsConnectionConfig {
table: string
data: Record<string, unknown>
conditions: Record<string, unknown>
}
export interface RdsDeleteParams extends RdsConnectionConfig {
table: string
conditions: Record<string, unknown>
}
export interface RdsExecuteParams extends RdsConnectionConfig {
query: string
}
export interface RdsBaseResponse extends ToolResponse {
output: {
message: string
rows: unknown[]
rowCount: number
}
error?: string
}
export interface RdsQueryResponse extends RdsBaseResponse {}
export interface RdsInsertResponse extends RdsBaseResponse {}
export interface RdsUpdateResponse extends RdsBaseResponse {}
export interface RdsDeleteResponse extends RdsBaseResponse {}
export interface RdsExecuteResponse extends RdsBaseResponse {}
export interface RdsResponse extends RdsBaseResponse {}

View File

@@ -0,0 +1,109 @@
import type { RdsUpdateParams, RdsUpdateResponse } from '@/tools/rds/types'
import type { ToolConfig } from '@/tools/types'
export const updateTool: ToolConfig<RdsUpdateParams, RdsUpdateResponse> = {
id: 'rds_update',
name: 'RDS Update',
description: 'Update data in an Amazon RDS table using the Data API',
version: '1.0',
params: {
region: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'AWS region (e.g., us-east-1)',
},
accessKeyId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'AWS access key ID',
},
secretAccessKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'AWS secret access key',
},
resourceArn: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ARN of the Aurora DB cluster',
},
secretArn: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ARN of the Secrets Manager secret containing DB credentials',
},
database: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Database name (optional)',
},
table: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Table name to update',
},
data: {
type: 'object',
required: true,
visibility: 'user-or-llm',
description: 'Data to update as key-value pairs',
},
conditions: {
type: 'object',
required: true,
visibility: 'user-or-llm',
description: 'Conditions for the update (e.g., {"id": 1})',
},
},
request: {
url: '/api/tools/rds/update',
method: 'POST',
headers: () => ({
'Content-Type': 'application/json',
}),
body: (params) => ({
region: params.region,
accessKeyId: params.accessKeyId,
secretAccessKey: params.secretAccessKey,
resourceArn: params.resourceArn,
secretArn: params.secretArn,
...(params.database && { database: params.database }),
table: params.table,
data: params.data,
conditions: params.conditions,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(data.error || 'RDS update failed')
}
return {
success: true,
output: {
message: data.message || 'Update executed successfully',
rows: data.rows || [],
rowCount: data.rowCount || 0,
},
error: undefined,
}
},
outputs: {
message: { type: 'string', description: 'Operation status message' },
rows: { type: 'array', description: 'Array of updated rows' },
rowCount: { type: 'number', description: 'Number of rows updated' },
},
}

View File

@@ -108,6 +108,14 @@ import {
discordUpdateMemberTool,
discordUpdateRoleTool,
} from '@/tools/discord'
import {
deleteTool as dynamodbDeleteTool,
getTool as dynamodbGetTool,
putTool as dynamodbPutTool,
queryTool as dynamodbQueryTool,
scanTool as dynamodbScanTool,
updateTool as dynamodbUpdateTool,
} from '@/tools/dynamodb'
import { elevenLabsTtsTool } from '@/tools/elevenlabs'
import {
exaAnswerTool,
@@ -725,6 +733,13 @@ import {
pylonUpdateUserTool,
} from '@/tools/pylon'
import { qdrantFetchTool, qdrantSearchTool, qdrantUpsertTool } from '@/tools/qdrant'
import {
deleteTool as rdsDeleteTool,
executeTool as rdsExecuteTool,
insertTool as rdsInsertTool,
queryTool as rdsQueryTool,
updateTool as rdsUpdateTool,
} from '@/tools/rds'
import {
redditDeleteTool,
redditEditTool,
@@ -1227,6 +1242,17 @@ export const tools: Record<string, ToolConfig> = {
postgresql_update: postgresUpdateTool,
postgresql_delete: postgresDeleteTool,
postgresql_execute: postgresExecuteTool,
rds_query: rdsQueryTool,
rds_insert: rdsInsertTool,
rds_update: rdsUpdateTool,
rds_delete: rdsDeleteTool,
rds_execute: rdsExecuteTool,
dynamodb_get: dynamodbGetTool,
dynamodb_put: dynamodbPutTool,
dynamodb_query: dynamodbQueryTool,
dynamodb_scan: dynamodbScanTool,
dynamodb_update: dynamodbUpdateTool,
dynamodb_delete: dynamodbDeleteTool,
mongodb_query: mongodbQueryTool,
mongodb_insert: mongodbInsertTool,
mongodb_update: mongodbUpdateTool,

View File

@@ -16,7 +16,7 @@ export const airtableWebhookTrigger: TriggerConfig = {
title: 'Credentials',
type: 'oauth-input',
description: 'This trigger requires airtable credentials to access your account.',
provider: 'airtable',
serviceId: 'airtable',
requiredScopes: [],
required: true,
mode: 'trigger',

View File

@@ -19,7 +19,7 @@ export const gmailPollingTrigger: TriggerConfig = {
title: 'Credentials',
type: 'oauth-input',
description: 'This trigger requires google email credentials to access your account.',
provider: 'google-email',
serviceId: 'gmail',
requiredScopes: [],
required: true,
mode: 'trigger',

View File

@@ -22,7 +22,6 @@ export const jiraWebhookSubBlocks: SubBlockConfig[] = [
id: 'triggerCredentials',
title: 'Jira Credentials',
type: 'oauth-input',
provider: 'jira',
serviceId: 'jira',
requiredScopes: [
'read:jira-work',

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