mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-09 23:17:59 -05:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
774e5d585c | ||
|
|
ede41af674 | ||
|
|
cb0c55c6f6 | ||
|
|
54cc93743f | ||
|
|
d22b21c8d1 |
File diff suppressed because one or more lines are too long
@@ -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,
|
||||
|
||||
193
apps/docs/content/docs/en/tools/dynamodb.mdx
Normal file
193
apps/docs/content/docs/en/tools/dynamodb.mdx
Normal 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`
|
||||
@@ -11,6 +11,7 @@
|
||||
"clay",
|
||||
"confluence",
|
||||
"discord",
|
||||
"dynamodb",
|
||||
"elevenlabs",
|
||||
"exa",
|
||||
"file",
|
||||
@@ -59,6 +60,7 @@
|
||||
"posthog",
|
||||
"pylon",
|
||||
"qdrant",
|
||||
"rds",
|
||||
"reddit",
|
||||
"resend",
|
||||
"s3",
|
||||
|
||||
173
apps/docs/content/docs/en/tools/rds.mdx
Normal file
173
apps/docs/content/docs/en/tools/rds.mdx
Normal 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`
|
||||
@@ -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 */}
|
||||
|
||||
@@ -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]',
|
||||
|
||||
41
apps/sim/app/api/tools/dynamodb/delete/route.ts
Normal file
41
apps/sim/app/api/tools/dynamodb/delete/route.ts
Normal 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 })
|
||||
}
|
||||
}
|
||||
48
apps/sim/app/api/tools/dynamodb/get/route.ts
Normal file
48
apps/sim/app/api/tools/dynamodb/get/route.ts
Normal 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 })
|
||||
}
|
||||
}
|
||||
36
apps/sim/app/api/tools/dynamodb/put/route.ts
Normal file
36
apps/sim/app/api/tools/dynamodb/put/route.ts
Normal 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 })
|
||||
}
|
||||
}
|
||||
51
apps/sim/app/api/tools/dynamodb/query/route.ts
Normal file
51
apps/sim/app/api/tools/dynamodb/query/route.ts
Normal 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 })
|
||||
}
|
||||
}
|
||||
45
apps/sim/app/api/tools/dynamodb/scan/route.ts
Normal file
45
apps/sim/app/api/tools/dynamodb/scan/route.ts
Normal 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 })
|
||||
}
|
||||
}
|
||||
50
apps/sim/app/api/tools/dynamodb/update/route.ts
Normal file
50
apps/sim/app/api/tools/dynamodb/update/route.ts
Normal 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 })
|
||||
}
|
||||
}
|
||||
174
apps/sim/app/api/tools/dynamodb/utils.ts
Normal file
174
apps/sim/app/api/tools/dynamodb/utils.ts
Normal 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 }
|
||||
}
|
||||
74
apps/sim/app/api/tools/rds/delete/route.ts
Normal file
74
apps/sim/app/api/tools/rds/delete/route.ts
Normal 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 })
|
||||
}
|
||||
}
|
||||
70
apps/sim/app/api/tools/rds/execute/route.ts
Normal file
70
apps/sim/app/api/tools/rds/execute/route.ts
Normal 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 })
|
||||
}
|
||||
}
|
||||
74
apps/sim/app/api/tools/rds/insert/route.ts
Normal file
74
apps/sim/app/api/tools/rds/insert/route.ts
Normal 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 })
|
||||
}
|
||||
}
|
||||
77
apps/sim/app/api/tools/rds/query/route.ts
Normal file
77
apps/sim/app/api/tools/rds/query/route.ts
Normal 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 })
|
||||
}
|
||||
}
|
||||
78
apps/sim/app/api/tools/rds/update/route.ts
Normal file
78
apps/sim/app/api/tools/rds/update/route.ts
Normal 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 })
|
||||
}
|
||||
}
|
||||
266
apps/sim/app/api/tools/rds/utils.ts
Normal file
266
apps/sim/app/api/tools/rds/utils.ts
Normal 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)
|
||||
}
|
||||
@@ -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',
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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) || ''
|
||||
)
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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' />
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -34,7 +34,6 @@ export const AsanaBlock: BlockConfig<AsanaResponse> = {
|
||||
type: 'oauth-input',
|
||||
|
||||
required: true,
|
||||
provider: 'asana',
|
||||
serviceId: 'asana',
|
||||
requiredScopes: ['default'],
|
||||
placeholder: 'Select Asana account',
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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',
|
||||
|
||||
372
apps/sim/blocks/blocks/dynamodb.ts
Normal file
372
apps/sim/blocks/blocks/dynamodb.ts
Normal 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',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -45,7 +45,6 @@ export const PipedriveBlock: BlockConfig<PipedriveResponse> = {
|
||||
id: 'credential',
|
||||
title: 'Pipedrive Account',
|
||||
type: 'oauth-input',
|
||||
provider: 'pipedrive',
|
||||
serviceId: 'pipedrive',
|
||||
requiredScopes: [
|
||||
'base',
|
||||
|
||||
326
apps/sim/blocks/blocks/rds.ts
Normal file
326
apps/sim/blocks/blocks/rds.ts
Normal 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',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -42,7 +42,6 @@ export const RedditBlock: BlockConfig<RedditResponse> = {
|
||||
id: 'credential',
|
||||
title: 'Reddit Account',
|
||||
type: 'oauth-input',
|
||||
provider: 'reddit',
|
||||
serviceId: 'reddit',
|
||||
requiredScopes: [
|
||||
'identity',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
84
apps/sim/tools/dynamodb/delete.ts
Normal file
84
apps/sim/tools/dynamodb/delete.ts
Normal 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' },
|
||||
},
|
||||
}
|
||||
86
apps/sim/tools/dynamodb/get.ts
Normal file
86
apps/sim/tools/dynamodb/get.ts
Normal 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' },
|
||||
},
|
||||
}
|
||||
8
apps/sim/tools/dynamodb/index.ts
Normal file
8
apps/sim/tools/dynamodb/index.ts
Normal 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 }
|
||||
79
apps/sim/tools/dynamodb/put.ts
Normal file
79
apps/sim/tools/dynamodb/put.ts
Normal 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' },
|
||||
},
|
||||
}
|
||||
120
apps/sim/tools/dynamodb/query.ts
Normal file
120
apps/sim/tools/dynamodb/query.ts
Normal 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' },
|
||||
},
|
||||
}
|
||||
113
apps/sim/tools/dynamodb/scan.ts
Normal file
113
apps/sim/tools/dynamodb/scan.ts
Normal 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' },
|
||||
},
|
||||
}
|
||||
70
apps/sim/tools/dynamodb/types.ts
Normal file
70
apps/sim/tools/dynamodb/types.ts
Normal 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 {}
|
||||
111
apps/sim/tools/dynamodb/update.ts
Normal file
111
apps/sim/tools/dynamodb/update.ts
Normal 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' },
|
||||
},
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
102
apps/sim/tools/rds/delete.ts
Normal file
102
apps/sim/tools/rds/delete.ts
Normal 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' },
|
||||
},
|
||||
}
|
||||
95
apps/sim/tools/rds/execute.ts
Normal file
95
apps/sim/tools/rds/execute.ts
Normal 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' },
|
||||
},
|
||||
}
|
||||
7
apps/sim/tools/rds/index.ts
Normal file
7
apps/sim/tools/rds/index.ts
Normal 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 }
|
||||
102
apps/sim/tools/rds/insert.ts
Normal file
102
apps/sim/tools/rds/insert.ts
Normal 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' },
|
||||
},
|
||||
}
|
||||
95
apps/sim/tools/rds/query.ts
Normal file
95
apps/sim/tools/rds/query.ts
Normal 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' },
|
||||
},
|
||||
}
|
||||
50
apps/sim/tools/rds/types.ts
Normal file
50
apps/sim/tools/rds/types.ts
Normal 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 {}
|
||||
109
apps/sim/tools/rds/update.ts
Normal file
109
apps/sim/tools/rds/update.ts
Normal 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' },
|
||||
},
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user