mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-08 22:48:14 -05:00
improvement(ux): added tab key navigation for agent messages, made variables styling match chat, added neo4j and calendly (#2056)
* improvement(ux): added tab key navigation for agent messages, made variables styling match chat * added neo4j tools, added calendly tools and triggers * more style improvements * consolidate wand generation type * more ui improvements * fix calendly * tested all neo4j tools * added fuzzy search for search modal, tested and fixed calendly * updated docs * fix various broken docs links, neo4j param validation * removed limit from neo4j block
This commit is contained in:
@@ -1802,38 +1802,38 @@ export function StripeIcon(props: SVGProps<SVGSVGElement>) {
|
||||
fillRule='evenodd'
|
||||
clipRule='evenodd'
|
||||
d='M360 78.0002C360 52.4002 347.6 32.2002 323.9 32.2002C300.1 32.2002 285.7 52.4002 285.7 77.8002C285.7 107.9 302.7 123.1 327.1 123.1C339 123.1 348 120.4 354.8 116.6V96.6002C348 100 340.2 102.1 330.3 102.1C320.6 102.1 312 98.7002 310.9 86.9002H359.8C359.8 85.6002 360 80.4002 360 78.0002ZM310.6 68.5002C310.6 57.2002 317.5 52.5002 323.8 52.5002C329.9 52.5002 336.4 57.2002 336.4 68.5002H310.6Z'
|
||||
fill='white'
|
||||
fill='currentColor'
|
||||
/>
|
||||
<path
|
||||
fillRule='evenodd'
|
||||
clipRule='evenodd'
|
||||
d='M247.1 32.2002C237.3 32.2002 231 36.8002 227.5 40.0002L226.2 33.8002H204.2V150.4L229.2 145.1L229.3 116.8C232.9 119.4 238.2 123.1 247 123.1C264.9 123.1 281.2 108.7 281.2 77.0002C281.1 48.0002 264.6 32.2002 247.1 32.2002ZM241.1 101.1C235.2 101.1 231.7 99.0002 229.3 96.4002L229.2 59.3002C231.8 56.4002 235.4 54.4002 241.1 54.4002C250.2 54.4002 256.5 64.6002 256.5 77.7002C256.5 91.1002 250.3 101.1 241.1 101.1Z'
|
||||
fill='white'
|
||||
fill='currentColor'
|
||||
/>
|
||||
<path
|
||||
fillRule='evenodd'
|
||||
clipRule='evenodd'
|
||||
d='M169.8 26.3001L194.9 20.9001V0.600098L169.8 5.9001V26.3001Z'
|
||||
fill='white'
|
||||
fill='currentColor'
|
||||
/>
|
||||
<path d='M194.9 33.9001H169.8V121.4H194.9V33.9001Z' fill='white' />
|
||||
<path d='M194.9 33.9001H169.8V121.4H194.9V33.9001Z' fill='currentColor' />
|
||||
<path
|
||||
fillRule='evenodd'
|
||||
clipRule='evenodd'
|
||||
d='M142.9 41.3001L141.3 33.9001H119.7V121.4H144.7V62.1001C150.6 54.4001 160.6 55.8001 163.7 56.9001V33.9001C160.5 32.7001 148.8 30.5001 142.9 41.3001Z'
|
||||
fill='white'
|
||||
fill='currentColor'
|
||||
/>
|
||||
<path
|
||||
fillRule='evenodd'
|
||||
clipRule='evenodd'
|
||||
d='M92.8999 12.2002L68.4999 17.4002L68.3999 97.5002C68.3999 112.3 79.4999 123.2 94.2999 123.2C102.5 123.2 108.5 121.7 111.8 119.9V99.6002C108.6 100.9 92.7999 105.5 92.7999 90.7002V55.2002H111.8V33.9002H92.7999L92.8999 12.2002Z'
|
||||
fill='white'
|
||||
fill='currentColor'
|
||||
/>
|
||||
<path
|
||||
fillRule='evenodd'
|
||||
clipRule='evenodd'
|
||||
d='M25.3 59.3002C25.3 55.4002 28.5 53.9002 33.8 53.9002C41.4 53.9002 51 56.2002 58.6 60.3002V36.8002C50.3 33.5002 42.1 32.2002 33.8 32.2002C13.5 32.2002 0 42.8002 0 60.5002C0 88.1002 38 83.7002 38 95.6002C38 100.2 34 101.7 28.4 101.7C20.1 101.7 9.5 98.3002 1.1 93.7002V117.5C10.4 121.5 19.8 123.2 28.4 123.2C49.2 123.2 63.5 112.9 63.5 95.0002C63.4 65.2002 25.3 70.5002 25.3 59.3002Z'
|
||||
fill='white'
|
||||
fill='currentColor'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
@@ -4032,3 +4032,55 @@ export function ApolloIcon(props: SVGProps<SVGSVGElement>) {
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function Neo4jIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} viewBox='0 0 128 128' fill='currentColor' xmlns='http://www.w3.org/2000/svg'>
|
||||
<path
|
||||
d='M63.333 32.567c-5.2.866-9.566 3-12.833 6.266-3.867 3.867-5.833 8.5-6.5 15.367-.3 3.133-.467 15.467-.2 15.467.067 0 .7-.234 1.4-.534 1.633-.7 5.167-.7 7-.033l1.4.5.167-8.033c.166-8.567.366-9.867 1.966-13.067 1.1-2.133 3.767-4.633 6.034-5.667 2.6-1.2 6.4-1.666 9.333-1.2 6.267 1.034 10 4.434 11.567 10.5.633 2.434.666 3.7.666 17.1v14.434H93.4L93.233 67.9c-.1-14.9-.166-15.9-.866-18.567-1.9-7.4-6.5-12.766-12.934-15.2-3.433-1.3-6.7-1.8-11.2-1.766-2.233.033-4.433.133-4.9.2z'
|
||||
fill='#000'
|
||||
/>
|
||||
<path
|
||||
d='M22.733 57.2c-2.866 1.433-4.4 4-4.4 7.467 0 1.1.2 2.5.467 3.133.633 1.567 2.433 3.467 4 4.3 1.9 1 5.5 1 7.367.033l1.366-.7 4.267 2.9 4.267 2.934V81.7L35.8 84.633l-4.3 2.934-1.1-.667c-1.6-.933-4.7-1.133-6.6-.4-2 .767-4.067 2.6-4.833 4.333-.834 1.767-.834 5.234 0 7 .7 1.567 2.333 3.3 3.8 4.067.6.3 2.033.6 3.233.7 2.8.2 5.167-.733 6.867-2.733 1.366-1.6 2.266-4.4 2.033-6.334l-.167-1.366 4.3-2.9 4.3-2.9 1.534.7c2.333 1 5.8.766 8-.567 2.4-1.5 3.6-3.633 3.733-6.633.1-2.1 0-2.567-.833-4.2-2.167-4.134-7-5.7-11.134-3.634l-1.233.6-4.233-2.9-4.234-2.9-.1-2.333c-.066-2.8-.866-4.6-2.833-6.233-2.5-2.134-6.233-2.567-9.267-1.067z'
|
||||
fill='#018BFF'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function CalendlyIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
viewBox='0 0 512 512'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
fillRule='evenodd'
|
||||
clipRule='evenodd'
|
||||
strokeLinejoin='round'
|
||||
strokeMiterlimit='2'
|
||||
>
|
||||
<g fillRule='nonzero'>
|
||||
<path
|
||||
d='M346.955 330.224c-15.875 14.088-35.7 31.619-71.647 31.619h-21.495c-26.012 0-49.672-9.455-66.607-26.593-16.543-16.747-25.649-39.665-25.649-64.545v-29.41c0-24.88 9.106-47.799 25.65-64.545 16.934-17.138 40.594-26.579 66.606-26.579h21.495c35.99 0 55.772 17.516 71.647 31.604 16.484 14.524 30.703 27.218 68.625 27.218a109.162 109.162 0 0017.269-1.38l-.13-.334a129.909 129.909 0 00-7.974-16.382L399.4 146.99c-23.232-40.234-66.304-65.098-112.763-65.096h-50.703c-46.46-.002-89.531 24.862-112.764 65.096l-25.344 43.906c-23.224 40.238-23.224 89.968 0 130.206l25.344 43.906c23.233 40.234 66.305 65.098 112.764 65.096h50.703c46.459.002 89.53-24.862 112.763-65.096l25.345-43.833a129.909 129.909 0 007.973-16.383l.13-.32a107.491 107.491 0 00-17.268-1.452c-37.922 0-52.14 12.621-68.625 27.218'
|
||||
fill='#006bff'
|
||||
/>
|
||||
<path
|
||||
d='M275.308 176.823h-21.495c-39.592 0-65.605 28.278-65.605 64.471v29.411c0 36.194 26.013 64.472 65.605 64.472h21.495c57.69 0 53.158-58.822 140.272-58.822 8.254-.009 16.49.75 24.603 2.266a130.047 130.047 0 000-45.242 134.431 134.431 0 01-24.603 2.266c-87.143 0-82.583-58.822-140.272-58.822'
|
||||
fill='#006bff'
|
||||
/>
|
||||
<path
|
||||
d='M490.233 300.116a121.451 121.451 0 00-50.035-21.51v.436a130.296 130.296 0 01-7.262 25.344 95.25 95.25 0 0141.364 17.037c0 .116-.072.261-.116.392-28.788 93.217-115.55 157.228-213.112 157.228-122.358 0-223.044-100.685-223.044-223.043S138.714 32.956 261.072 32.956c97.561 0 184.324 64.012 213.112 157.229 0 .13.073.276.116.392a95.073 95.073 0 01-41.364 17.022 131.112 131.112 0 017.262 25.373 3.166 3.166 0 000 .407 121.415 121.415 0 0050.035-21.495c14.262-10.56 11.503-22.483 9.339-29.542C467.34 77.803 370.064 6 260.67 6c-137.147 0-250 112.854-250 250 0 137.146 112.853 250 250 250 109.394 0 206.67-71.803 238.902-176.342 2.164-7.059 4.923-18.983-9.34-29.542'
|
||||
fill='#006bff'
|
||||
/>
|
||||
<path
|
||||
d='M432.849 207.599a107.491 107.491 0 01-17.269 1.452c-37.922 0-52.14-12.62-68.61-27.217-15.89-14.089-35.672-31.619-71.662-31.619h-21.495c-26.027 0-49.672 9.455-66.607 26.593-16.543 16.746-25.649 39.665-25.649 64.545v29.41c0 24.88 9.106 47.799 25.65 64.545 16.934 17.138 40.579 26.578 66.606 26.578h21.495c35.99 0 55.772-17.515 71.661-31.604 16.47-14.524 30.69-27.217 68.611-27.217 5.783.001 11.558.463 17.269 1.38a129.303 129.303 0 007.262-25.345c.009-.145.009-.29 0-.436a134.301 134.301 0 00-24.604-2.25c-87.143 0-82.583 58.836-140.271 58.836H253.74c-39.592 0-65.604-28.293-65.604-64.487v-29.469c0-36.193 26.012-64.471 65.604-64.471h21.496c57.688 0 53.157 58.807 140.271 58.807 8.254.015 16.49-.74 24.604-2.251v-.407a131.112 131.112 0 00-7.262-25.373'
|
||||
fill='#0ae8f0'
|
||||
/>
|
||||
<path
|
||||
d='M432.849 207.599a107.491 107.491 0 01-17.269 1.452c-37.922 0-52.14-12.62-68.61-27.217-15.89-14.089-35.672-31.619-71.662-31.619h-21.495c-26.027 0-49.672 9.455-66.607 26.593-16.543 16.746-25.649 39.665-25.649 64.545v29.41c0 24.88 9.106 47.799 25.65 64.545 16.934 17.138 40.579 26.578 66.606 26.578h21.495c35.99 0 55.772-17.515 71.661-31.604 16.47-14.524 30.69-27.217 68.611-27.217 5.783.001 11.558.463 17.269 1.38a129.303 129.303 0 007.262-25.345c.009-.145.009-.29 0-.436a134.301 134.301 0 00-24.604-2.25c-87.143 0-82.583 58.836-140.271 58.836H253.74c-39.592 0-65.604-28.293-65.604-64.487v-29.469c0-36.193 26.012-64.471 65.604-64.471h21.496c57.688 0 53.157 58.807 140.271 58.807 8.254.015 16.49-.74 24.604-2.251v-.407a131.112 131.112 0 00-7.262-25.373'
|
||||
fill='#0ae8f0'
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
AsanaIcon,
|
||||
BrainIcon,
|
||||
BrowserUseIcon,
|
||||
CalendlyIcon,
|
||||
ClayIcon,
|
||||
ConfluenceIcon,
|
||||
DiscordIcon,
|
||||
@@ -44,6 +45,7 @@ import {
|
||||
MistralIcon,
|
||||
MongoDBIcon,
|
||||
MySQLIcon,
|
||||
Neo4jIcon,
|
||||
NotionIcon,
|
||||
OpenAIIcon,
|
||||
OutlookIcon,
|
||||
@@ -118,6 +120,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
|
||||
openai: OpenAIIcon,
|
||||
onedrive: MicrosoftOneDriveIcon,
|
||||
notion: NotionIcon,
|
||||
neo4j: Neo4jIcon,
|
||||
mysql: MySQLIcon,
|
||||
mongodb: MongoDBIcon,
|
||||
mistral_parse: MistralIcon,
|
||||
@@ -151,6 +154,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
|
||||
discord: DiscordIcon,
|
||||
confluence: ConfluenceIcon,
|
||||
clay: ClayIcon,
|
||||
calendly: CalendlyIcon,
|
||||
browser_use: BrowserUseIcon,
|
||||
asana: AsanaIcon,
|
||||
arxiv: ArxivIcon,
|
||||
|
||||
163
apps/docs/content/docs/en/tools/calendly.mdx
Normal file
163
apps/docs/content/docs/en/tools/calendly.mdx
Normal file
@@ -0,0 +1,163 @@
|
||||
---
|
||||
title: Calendly
|
||||
description: Manage Calendly scheduling and events
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="calendly"
|
||||
color="#FFFFFF"
|
||||
/>
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Integrate Calendly into your workflow. Manage event types, scheduled events, invitees, and webhooks. Can also trigger workflows based on Calendly webhook events (invitee scheduled, invitee canceled, routing form submitted). Requires Personal Access Token.
|
||||
|
||||
|
||||
|
||||
## Tools
|
||||
|
||||
### `calendly_get_current_user`
|
||||
|
||||
Get information about the currently authenticated Calendly user
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Calendly Personal Access Token |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `resource` | object | Current user information |
|
||||
|
||||
### `calendly_list_event_types`
|
||||
|
||||
Retrieve a list of all event types for a user or organization
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Calendly Personal Access Token |
|
||||
| `user` | string | No | Return only event types that belong to this user \(URI format\) |
|
||||
| `organization` | string | No | Return only event types that belong to this organization \(URI format\) |
|
||||
| `count` | number | No | Number of results per page \(default: 20, max: 100\) |
|
||||
| `pageToken` | string | No | Page token for pagination |
|
||||
| `sort` | string | No | Sort order for results \(e.g., "name:asc", "name:desc"\) |
|
||||
| `active` | boolean | No | When true, show only active event types. When false or unchecked, show all event types \(both active and inactive\). |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `collection` | array | Array of event type objects |
|
||||
|
||||
### `calendly_get_event_type`
|
||||
|
||||
Get detailed information about a specific event type
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Calendly Personal Access Token |
|
||||
| `eventTypeUuid` | string | Yes | Event type UUID \(can be full URI or just the UUID\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `resource` | object | Event type details |
|
||||
|
||||
### `calendly_list_scheduled_events`
|
||||
|
||||
Retrieve a list of scheduled events for a user or organization
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Calendly Personal Access Token |
|
||||
| `user` | string | No | Return events that belong to this user \(URI format\). Either "user" or "organization" must be provided. |
|
||||
| `organization` | string | No | Return events that belong to this organization \(URI format\). Either "user" or "organization" must be provided. |
|
||||
| `invitee_email` | string | No | Return events where invitee has this email |
|
||||
| `count` | number | No | Number of results per page \(default: 20, max: 100\) |
|
||||
| `max_start_time` | string | No | Return events with start time before this time \(ISO 8601 format\) |
|
||||
| `min_start_time` | string | No | Return events with start time after this time \(ISO 8601 format\) |
|
||||
| `pageToken` | string | No | Page token for pagination |
|
||||
| `sort` | string | No | Sort order for results \(e.g., "start_time:asc", "start_time:desc"\) |
|
||||
| `status` | string | No | Filter by status \("active" or "canceled"\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `collection` | array | Array of scheduled event objects |
|
||||
|
||||
### `calendly_get_scheduled_event`
|
||||
|
||||
Get detailed information about a specific scheduled event
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Calendly Personal Access Token |
|
||||
| `eventUuid` | string | Yes | Scheduled event UUID \(can be full URI or just the UUID\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `resource` | object | Scheduled event details |
|
||||
|
||||
### `calendly_list_event_invitees`
|
||||
|
||||
Retrieve a list of invitees for a scheduled event
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Calendly Personal Access Token |
|
||||
| `eventUuid` | string | Yes | Scheduled event UUID \(can be full URI or just the UUID\) |
|
||||
| `count` | number | No | Number of results per page \(default: 20, max: 100\) |
|
||||
| `email` | string | No | Filter invitees by email address |
|
||||
| `pageToken` | string | No | Page token for pagination |
|
||||
| `sort` | string | No | Sort order for results \(e.g., "created_at:asc", "created_at:desc"\) |
|
||||
| `status` | string | No | Filter by status \("active" or "canceled"\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `collection` | array | Array of invitee objects |
|
||||
|
||||
### `calendly_cancel_event`
|
||||
|
||||
Cancel a scheduled event
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Calendly Personal Access Token |
|
||||
| `eventUuid` | string | Yes | Scheduled event UUID to cancel \(can be full URI or just the UUID\) |
|
||||
| `reason` | string | No | Reason for cancellation \(will be sent to invitees\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `resource` | object | Cancellation details |
|
||||
|
||||
|
||||
|
||||
## Notes
|
||||
|
||||
- Category: `tools`
|
||||
- Type: `calendly`
|
||||
@@ -26,9 +26,11 @@ Add a new memory to the database or append to existing memory with the same ID.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `id` | string | Yes | Identifier for the memory. If a memory with this ID already exists, the new data will be appended to it. |
|
||||
| `conversationId` | string | No | Conversation identifier \(e.g., user-123, session-abc\). If a memory with this conversationId already exists for this block, the new message will be appended to it. |
|
||||
| `id` | string | No | Legacy parameter for conversation identifier. Use conversationId instead. Provided for backwards compatibility. |
|
||||
| `role` | string | Yes | Role for agent memory \(user, assistant, or system\) |
|
||||
| `content` | string | Yes | Content for agent memory |
|
||||
| `blockId` | string | No | Optional block ID. If not provided, uses the current block ID from execution context, or defaults to "default". |
|
||||
|
||||
#### Output
|
||||
|
||||
@@ -40,20 +42,23 @@ Add a new memory to the database or append to existing memory with the same ID.
|
||||
|
||||
### `memory_get`
|
||||
|
||||
Retrieve a specific memory by its ID
|
||||
Retrieve memory by conversationId, blockId, blockName, or a combination. Returns all matching memories.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `id` | string | Yes | Identifier for the memory to retrieve |
|
||||
| `conversationId` | string | No | Conversation identifier \(e.g., user-123, session-abc\). If provided alone, returns all memories for this conversation across all blocks. |
|
||||
| `id` | string | No | Legacy parameter for conversation identifier. Use conversationId instead. Provided for backwards compatibility. |
|
||||
| `blockId` | string | No | Block identifier. If provided alone, returns all memories for this block across all conversations. If provided with conversationId, returns memories for that specific conversation in this block. |
|
||||
| `blockName` | string | No | Block name. Alternative to blockId. If provided alone, returns all memories for blocks with this name. If provided with conversationId, returns memories for that conversation in blocks with this name. |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Whether the memory was retrieved successfully |
|
||||
| `memories` | array | Array of memory data for the requested ID |
|
||||
| `memories` | array | Array of memory objects with conversationId, blockId, blockName, and data fields |
|
||||
| `message` | string | Success or error message |
|
||||
| `error` | string | Error message if operation failed |
|
||||
|
||||
@@ -71,19 +76,22 @@ Retrieve all memories from the database
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Whether all memories were retrieved successfully |
|
||||
| `memories` | array | Array of all memory objects with keys, types, and data |
|
||||
| `memories` | array | Array of all memory objects with key, conversationId, blockId, blockName, and data fields |
|
||||
| `message` | string | Success or error message |
|
||||
| `error` | string | Error message if operation failed |
|
||||
|
||||
### `memory_delete`
|
||||
|
||||
Delete a specific memory by its ID
|
||||
Delete memories by conversationId, blockId, blockName, or a combination. Supports bulk deletion.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `id` | string | Yes | Identifier for the memory to delete |
|
||||
| `conversationId` | string | No | Conversation identifier \(e.g., user-123, session-abc\). If provided alone, deletes all memories for this conversation across all blocks. |
|
||||
| `id` | string | No | Legacy parameter for conversation identifier. Use conversationId instead. Provided for backwards compatibility. |
|
||||
| `blockId` | string | No | Block identifier. If provided alone, deletes all memories for this block across all conversations. If provided with conversationId, deletes memories for that specific conversation in this block. |
|
||||
| `blockName` | string | No | Block name. Alternative to blockId. If provided alone, deletes all memories for blocks with this name. If provided with conversationId, deletes memories for that conversation in blocks with this name. |
|
||||
|
||||
#### Output
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"arxiv",
|
||||
"asana",
|
||||
"browser_use",
|
||||
"calendly",
|
||||
"clay",
|
||||
"confluence",
|
||||
"discord",
|
||||
@@ -39,6 +40,7 @@
|
||||
"mistral_parse",
|
||||
"mongodb",
|
||||
"mysql",
|
||||
"neo4j",
|
||||
"notion",
|
||||
"onedrive",
|
||||
"openai",
|
||||
|
||||
176
apps/docs/content/docs/en/tools/neo4j.mdx
Normal file
176
apps/docs/content/docs/en/tools/neo4j.mdx
Normal file
@@ -0,0 +1,176 @@
|
||||
---
|
||||
title: Neo4j
|
||||
description: Connect to Neo4j graph database
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="neo4j"
|
||||
color="#FFFFFF"
|
||||
/>
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Integrate Neo4j graph database into the workflow. Can query, create, merge, update, and delete nodes and relationships.
|
||||
|
||||
|
||||
|
||||
## Tools
|
||||
|
||||
### `neo4j_query`
|
||||
|
||||
Execute MATCH queries to read nodes and relationships from Neo4j graph database. For best performance and to prevent large result sets, include LIMIT in your query (e.g.,
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `host` | string | Yes | Neo4j server hostname or IP address |
|
||||
| `port` | number | Yes | Neo4j server port \(default: 7687 for Bolt protocol\) |
|
||||
| `database` | string | Yes | Database name to connect to |
|
||||
| `username` | string | Yes | Neo4j username |
|
||||
| `password` | string | Yes | Neo4j password |
|
||||
| `encryption` | string | No | Connection encryption mode \(enabled, disabled\) |
|
||||
| `cypherQuery` | string | Yes | Cypher query to execute \(typically MATCH statements\) |
|
||||
| `parameters` | object | No | Parameters for the Cypher query as a JSON object. Use for any dynamic values including LIMIT \(e.g., query: "MATCH \(n\) RETURN n LIMIT $limit", parameters: \{limit: 100\}\). |
|
||||
| `parameters` | string | No | No description |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `message` | string | Operation status message |
|
||||
| `records` | array | Array of records returned from the query |
|
||||
| `recordCount` | number | Number of records returned |
|
||||
| `summary` | json | Query execution summary with timing and counters |
|
||||
|
||||
### `neo4j_create`
|
||||
|
||||
Execute CREATE statements to add new nodes and relationships to Neo4j graph database
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `host` | string | Yes | Neo4j server hostname or IP address |
|
||||
| `port` | number | Yes | Neo4j server port \(default: 7687 for Bolt protocol\) |
|
||||
| `database` | string | Yes | Database name to connect to |
|
||||
| `username` | string | Yes | Neo4j username |
|
||||
| `password` | string | Yes | Neo4j password |
|
||||
| `encryption` | string | No | Connection encryption mode \(enabled, disabled\) |
|
||||
| `cypherQuery` | string | Yes | Cypher CREATE statement to execute |
|
||||
| `parameters` | object | No | Parameters for the Cypher query as a JSON object |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `message` | string | Operation status message |
|
||||
| `summary` | json | Creation summary with counters for nodes and relationships created |
|
||||
|
||||
### `neo4j_merge`
|
||||
|
||||
Execute MERGE statements to find or create nodes and relationships in Neo4j (upsert operation)
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `host` | string | Yes | Neo4j server hostname or IP address |
|
||||
| `port` | number | Yes | Neo4j server port \(default: 7687 for Bolt protocol\) |
|
||||
| `database` | string | Yes | Database name to connect to |
|
||||
| `username` | string | Yes | Neo4j username |
|
||||
| `password` | string | Yes | Neo4j password |
|
||||
| `encryption` | string | No | Connection encryption mode \(enabled, disabled\) |
|
||||
| `cypherQuery` | string | Yes | Cypher MERGE statement to execute |
|
||||
| `parameters` | object | No | Parameters for the Cypher query as a JSON object |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `message` | string | Operation status message |
|
||||
| `summary` | json | Merge summary with counters for nodes/relationships created or matched |
|
||||
|
||||
### `neo4j_update`
|
||||
|
||||
Execute SET statements to update properties of existing nodes and relationships in Neo4j
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `host` | string | Yes | Neo4j server hostname or IP address |
|
||||
| `port` | number | Yes | Neo4j server port \(default: 7687 for Bolt protocol\) |
|
||||
| `database` | string | Yes | Database name to connect to |
|
||||
| `username` | string | Yes | Neo4j username |
|
||||
| `password` | string | Yes | Neo4j password |
|
||||
| `encryption` | string | No | Connection encryption mode \(enabled, disabled\) |
|
||||
| `cypherQuery` | string | Yes | Cypher query with MATCH and SET statements to update properties |
|
||||
| `parameters` | object | No | Parameters for the Cypher query as a JSON object |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `message` | string | Operation status message |
|
||||
| `summary` | json | Update summary with counters for properties set |
|
||||
|
||||
### `neo4j_delete`
|
||||
|
||||
Execute DELETE or DETACH DELETE statements to remove nodes and relationships from Neo4j
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `host` | string | Yes | Neo4j server hostname or IP address |
|
||||
| `port` | number | Yes | Neo4j server port \(default: 7687 for Bolt protocol\) |
|
||||
| `database` | string | Yes | Database name to connect to |
|
||||
| `username` | string | Yes | Neo4j username |
|
||||
| `password` | string | Yes | Neo4j password |
|
||||
| `encryption` | string | No | Connection encryption mode \(enabled, disabled\) |
|
||||
| `cypherQuery` | string | Yes | Cypher query with MATCH and DELETE/DETACH DELETE statements |
|
||||
| `parameters` | object | No | Parameters for the Cypher query as a JSON object |
|
||||
| `detach` | boolean | No | Whether to use DETACH DELETE to remove relationships before deleting nodes |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `message` | string | Operation status message |
|
||||
| `summary` | json | Delete summary with counters for nodes and relationships deleted |
|
||||
|
||||
### `neo4j_execute`
|
||||
|
||||
Execute arbitrary Cypher queries on Neo4j graph database for complex operations
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `host` | string | Yes | Neo4j server hostname or IP address |
|
||||
| `port` | number | Yes | Neo4j server port \(default: 7687 for Bolt protocol\) |
|
||||
| `database` | string | Yes | Database name to connect to |
|
||||
| `username` | string | Yes | Neo4j username |
|
||||
| `password` | string | Yes | Neo4j password |
|
||||
| `encryption` | string | No | Connection encryption mode \(enabled, disabled\) |
|
||||
| `cypherQuery` | string | Yes | Cypher query to execute \(any valid Cypher statement\) |
|
||||
| `parameters` | object | No | Parameters for the Cypher query as a JSON object |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `message` | string | Operation status message |
|
||||
| `records` | array | Array of records returned from the query |
|
||||
| `recordCount` | number | Number of records returned |
|
||||
| `summary` | json | Execution summary with timing and counters |
|
||||
|
||||
|
||||
|
||||
## Notes
|
||||
|
||||
- Category: `tools`
|
||||
- Type: `neo4j`
|
||||
@@ -1,7 +1,6 @@
|
||||
import * as Icons from '@/components/icons'
|
||||
import { inter } from '@/app/fonts/inter/inter'
|
||||
|
||||
// AI models and providers
|
||||
const modelProviderIcons = [
|
||||
{ icon: Icons.OpenAIIcon, label: 'OpenAI' },
|
||||
{ icon: Icons.AnthropicIcon, label: 'Anthropic' },
|
||||
@@ -16,7 +15,6 @@ const modelProviderIcons = [
|
||||
{ icon: Icons.ElevenLabsIcon, label: 'ElevenLabs' },
|
||||
]
|
||||
|
||||
// Communication and productivity tools
|
||||
const communicationIcons = [
|
||||
{ icon: Icons.SlackIcon, label: 'Slack' },
|
||||
{ icon: Icons.GmailIcon, label: 'Gmail' },
|
||||
@@ -28,6 +26,7 @@ const communicationIcons = [
|
||||
{ icon: Icons.ConfluenceIcon, label: 'Confluence' },
|
||||
{ icon: Icons.TelegramIcon, label: 'Telegram' },
|
||||
{ icon: Icons.GoogleCalendarIcon, label: 'Google Calendar' },
|
||||
{ icon: Icons.CalendlyIcon, label: 'Calendly' },
|
||||
{ icon: Icons.GoogleDocsIcon, label: 'Google Docs' },
|
||||
{ icon: Icons.BrowserUseIcon, label: 'BrowserUse' },
|
||||
{ icon: Icons.TypeformIcon, label: 'Typeform' },
|
||||
@@ -37,7 +36,6 @@ const communicationIcons = [
|
||||
{ icon: Icons.AirtableIcon, label: 'Airtable' },
|
||||
]
|
||||
|
||||
// Data, storage and search services
|
||||
const dataStorageIcons = [
|
||||
{ icon: Icons.PineconeIcon, label: 'Pinecone' },
|
||||
{ icon: Icons.SupabaseIcon, label: 'Supabase' },
|
||||
|
||||
118
apps/sim/app/api/tools/neo4j/create/route.ts
Normal file
118
apps/sim/app/api/tools/neo4j/create/route.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import { randomUUID } from 'crypto'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import {
|
||||
convertNeo4jTypesToJSON,
|
||||
createNeo4jDriver,
|
||||
validateCypherQuery,
|
||||
} from '@/app/api/tools/neo4j/utils'
|
||||
|
||||
const logger = createLogger('Neo4jCreateAPI')
|
||||
|
||||
const CreateSchema = z.object({
|
||||
host: z.string().min(1, 'Host is required'),
|
||||
port: z.coerce.number().int().positive('Port must be a positive integer'),
|
||||
database: z.string().min(1, 'Database name is required'),
|
||||
username: z.string().min(1, 'Username is required'),
|
||||
password: z.string().min(1, 'Password is required'),
|
||||
encryption: z.enum(['enabled', 'disabled']).default('disabled'),
|
||||
cypherQuery: z.string().min(1, 'Cypher query is required'),
|
||||
parameters: z.record(z.unknown()).nullable().optional().default({}),
|
||||
})
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const requestId = randomUUID().slice(0, 8)
|
||||
let driver = null
|
||||
let session = null
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const params = CreateSchema.parse(body)
|
||||
|
||||
logger.info(
|
||||
`[${requestId}] Executing Neo4j create on ${params.host}:${params.port}/${params.database}`
|
||||
)
|
||||
|
||||
const validation = validateCypherQuery(params.cypherQuery)
|
||||
if (!validation.isValid) {
|
||||
logger.warn(`[${requestId}] Cypher query validation failed: ${validation.error}`)
|
||||
return NextResponse.json(
|
||||
{ error: `Query validation failed: ${validation.error}` },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
driver = await createNeo4jDriver({
|
||||
host: params.host,
|
||||
port: params.port,
|
||||
database: params.database,
|
||||
username: params.username,
|
||||
password: params.password,
|
||||
encryption: params.encryption,
|
||||
})
|
||||
|
||||
session = driver.session({ database: params.database })
|
||||
|
||||
const result = await session.run(params.cypherQuery, params.parameters)
|
||||
|
||||
const records = result.records.map((record) => {
|
||||
const obj: Record<string, unknown> = {}
|
||||
record.keys.forEach((key) => {
|
||||
if (typeof key === 'string') {
|
||||
obj[key] = convertNeo4jTypesToJSON(record.get(key))
|
||||
}
|
||||
})
|
||||
return obj
|
||||
})
|
||||
|
||||
const summary = {
|
||||
resultAvailableAfter: result.summary.resultAvailableAfter.toNumber(),
|
||||
resultConsumedAfter: result.summary.resultConsumedAfter.toNumber(),
|
||||
counters: {
|
||||
nodesCreated: result.summary.counters.updates().nodesCreated,
|
||||
nodesDeleted: result.summary.counters.updates().nodesDeleted,
|
||||
relationshipsCreated: result.summary.counters.updates().relationshipsCreated,
|
||||
relationshipsDeleted: result.summary.counters.updates().relationshipsDeleted,
|
||||
propertiesSet: result.summary.counters.updates().propertiesSet,
|
||||
labelsAdded: result.summary.counters.updates().labelsAdded,
|
||||
labelsRemoved: result.summary.counters.updates().labelsRemoved,
|
||||
indexesAdded: result.summary.counters.updates().indexesAdded,
|
||||
indexesRemoved: result.summary.counters.updates().indexesRemoved,
|
||||
constraintsAdded: result.summary.counters.updates().constraintsAdded,
|
||||
constraintsRemoved: result.summary.counters.updates().constraintsRemoved,
|
||||
},
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`[${requestId}] Create executed successfully, created ${summary.counters.nodesCreated} nodes and ${summary.counters.relationshipsCreated} relationships, returned ${records.length} records`
|
||||
)
|
||||
|
||||
return NextResponse.json({
|
||||
message: `Created ${summary.counters.nodesCreated} nodes and ${summary.counters.relationshipsCreated} relationships`,
|
||||
records,
|
||||
recordCount: records.length,
|
||||
summary,
|
||||
})
|
||||
} 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}] Neo4j create failed:`, error)
|
||||
|
||||
return NextResponse.json({ error: `Neo4j create failed: ${errorMessage}` }, { status: 500 })
|
||||
} finally {
|
||||
if (session) {
|
||||
await session.close()
|
||||
}
|
||||
if (driver) {
|
||||
await driver.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
103
apps/sim/app/api/tools/neo4j/delete/route.ts
Normal file
103
apps/sim/app/api/tools/neo4j/delete/route.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import { randomUUID } from 'crypto'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { createNeo4jDriver, validateCypherQuery } from '@/app/api/tools/neo4j/utils'
|
||||
|
||||
const logger = createLogger('Neo4jDeleteAPI')
|
||||
|
||||
const DeleteSchema = z.object({
|
||||
host: z.string().min(1, 'Host is required'),
|
||||
port: z.coerce.number().int().positive('Port must be a positive integer'),
|
||||
database: z.string().min(1, 'Database name is required'),
|
||||
username: z.string().min(1, 'Username is required'),
|
||||
password: z.string().min(1, 'Password is required'),
|
||||
encryption: z.enum(['enabled', 'disabled']).default('disabled'),
|
||||
cypherQuery: z.string().min(1, 'Cypher query is required'),
|
||||
parameters: z.record(z.unknown()).nullable().optional().default({}),
|
||||
detach: z.boolean().optional().default(false),
|
||||
})
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const requestId = randomUUID().slice(0, 8)
|
||||
let driver = null
|
||||
let session = null
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const params = DeleteSchema.parse(body)
|
||||
|
||||
logger.info(
|
||||
`[${requestId}] Executing Neo4j delete on ${params.host}:${params.port}/${params.database}`
|
||||
)
|
||||
|
||||
const validation = validateCypherQuery(params.cypherQuery)
|
||||
if (!validation.isValid) {
|
||||
logger.warn(`[${requestId}] Cypher query validation failed: ${validation.error}`)
|
||||
return NextResponse.json(
|
||||
{ error: `Query validation failed: ${validation.error}` },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
driver = await createNeo4jDriver({
|
||||
host: params.host,
|
||||
port: params.port,
|
||||
database: params.database,
|
||||
username: params.username,
|
||||
password: params.password,
|
||||
encryption: params.encryption,
|
||||
})
|
||||
|
||||
session = driver.session({ database: params.database })
|
||||
|
||||
const result = await session.run(params.cypherQuery, params.parameters)
|
||||
|
||||
const summary = {
|
||||
resultAvailableAfter: result.summary.resultAvailableAfter.toNumber(),
|
||||
resultConsumedAfter: result.summary.resultConsumedAfter.toNumber(),
|
||||
counters: {
|
||||
nodesCreated: result.summary.counters.updates().nodesCreated,
|
||||
nodesDeleted: result.summary.counters.updates().nodesDeleted,
|
||||
relationshipsCreated: result.summary.counters.updates().relationshipsCreated,
|
||||
relationshipsDeleted: result.summary.counters.updates().relationshipsDeleted,
|
||||
propertiesSet: result.summary.counters.updates().propertiesSet,
|
||||
labelsAdded: result.summary.counters.updates().labelsAdded,
|
||||
labelsRemoved: result.summary.counters.updates().labelsRemoved,
|
||||
indexesAdded: result.summary.counters.updates().indexesAdded,
|
||||
indexesRemoved: result.summary.counters.updates().indexesRemoved,
|
||||
constraintsAdded: result.summary.counters.updates().constraintsAdded,
|
||||
constraintsRemoved: result.summary.counters.updates().constraintsRemoved,
|
||||
},
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`[${requestId}] Delete executed successfully, deleted ${summary.counters.nodesDeleted} nodes and ${summary.counters.relationshipsDeleted} relationships`
|
||||
)
|
||||
|
||||
return NextResponse.json({
|
||||
message: `Deleted ${summary.counters.nodesDeleted} nodes and ${summary.counters.relationshipsDeleted} relationships`,
|
||||
summary,
|
||||
})
|
||||
} 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}] Neo4j delete failed:`, error)
|
||||
|
||||
return NextResponse.json({ error: `Neo4j delete failed: ${errorMessage}` }, { status: 500 })
|
||||
} finally {
|
||||
if (session) {
|
||||
await session.close()
|
||||
}
|
||||
if (driver) {
|
||||
await driver.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
116
apps/sim/app/api/tools/neo4j/execute/route.ts
Normal file
116
apps/sim/app/api/tools/neo4j/execute/route.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import { randomUUID } from 'crypto'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import {
|
||||
convertNeo4jTypesToJSON,
|
||||
createNeo4jDriver,
|
||||
validateCypherQuery,
|
||||
} from '@/app/api/tools/neo4j/utils'
|
||||
|
||||
const logger = createLogger('Neo4jExecuteAPI')
|
||||
|
||||
const ExecuteSchema = z.object({
|
||||
host: z.string().min(1, 'Host is required'),
|
||||
port: z.coerce.number().int().positive('Port must be a positive integer'),
|
||||
database: z.string().min(1, 'Database name is required'),
|
||||
username: z.string().min(1, 'Username is required'),
|
||||
password: z.string().min(1, 'Password is required'),
|
||||
encryption: z.enum(['enabled', 'disabled']).default('disabled'),
|
||||
cypherQuery: z.string().min(1, 'Cypher query is required'),
|
||||
parameters: z.record(z.unknown()).nullable().optional().default({}),
|
||||
})
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const requestId = randomUUID().slice(0, 8)
|
||||
let driver = null
|
||||
let session = null
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const params = ExecuteSchema.parse(body)
|
||||
|
||||
logger.info(
|
||||
`[${requestId}] Executing Neo4j query on ${params.host}:${params.port}/${params.database}`
|
||||
)
|
||||
|
||||
const validation = validateCypherQuery(params.cypherQuery)
|
||||
if (!validation.isValid) {
|
||||
logger.warn(`[${requestId}] Cypher query validation failed: ${validation.error}`)
|
||||
return NextResponse.json(
|
||||
{ error: `Query validation failed: ${validation.error}` },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
driver = await createNeo4jDriver({
|
||||
host: params.host,
|
||||
port: params.port,
|
||||
database: params.database,
|
||||
username: params.username,
|
||||
password: params.password,
|
||||
encryption: params.encryption,
|
||||
})
|
||||
|
||||
session = driver.session({ database: params.database })
|
||||
|
||||
const result = await session.run(params.cypherQuery, params.parameters)
|
||||
|
||||
const records = result.records.map((record) => {
|
||||
const obj: Record<string, unknown> = {}
|
||||
record.keys.forEach((key) => {
|
||||
if (typeof key === 'string') {
|
||||
obj[key] = convertNeo4jTypesToJSON(record.get(key))
|
||||
}
|
||||
})
|
||||
return obj
|
||||
})
|
||||
|
||||
const summary = {
|
||||
resultAvailableAfter: result.summary.resultAvailableAfter.toNumber(),
|
||||
resultConsumedAfter: result.summary.resultConsumedAfter.toNumber(),
|
||||
counters: {
|
||||
nodesCreated: result.summary.counters.updates().nodesCreated,
|
||||
nodesDeleted: result.summary.counters.updates().nodesDeleted,
|
||||
relationshipsCreated: result.summary.counters.updates().relationshipsCreated,
|
||||
relationshipsDeleted: result.summary.counters.updates().relationshipsDeleted,
|
||||
propertiesSet: result.summary.counters.updates().propertiesSet,
|
||||
labelsAdded: result.summary.counters.updates().labelsAdded,
|
||||
labelsRemoved: result.summary.counters.updates().labelsRemoved,
|
||||
indexesAdded: result.summary.counters.updates().indexesAdded,
|
||||
indexesRemoved: result.summary.counters.updates().indexesRemoved,
|
||||
constraintsAdded: result.summary.counters.updates().constraintsAdded,
|
||||
constraintsRemoved: result.summary.counters.updates().constraintsRemoved,
|
||||
},
|
||||
}
|
||||
|
||||
logger.info(`[${requestId}] Query executed successfully, returned ${records.length} records`)
|
||||
|
||||
return NextResponse.json({
|
||||
message: `Query executed successfully, returned ${records.length} records`,
|
||||
records,
|
||||
recordCount: records.length,
|
||||
summary,
|
||||
})
|
||||
} 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}] Neo4j execute failed:`, error)
|
||||
|
||||
return NextResponse.json({ error: `Neo4j execute failed: ${errorMessage}` }, { status: 500 })
|
||||
} finally {
|
||||
if (session) {
|
||||
await session.close()
|
||||
}
|
||||
if (driver) {
|
||||
await driver.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
118
apps/sim/app/api/tools/neo4j/merge/route.ts
Normal file
118
apps/sim/app/api/tools/neo4j/merge/route.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import { randomUUID } from 'crypto'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import {
|
||||
convertNeo4jTypesToJSON,
|
||||
createNeo4jDriver,
|
||||
validateCypherQuery,
|
||||
} from '@/app/api/tools/neo4j/utils'
|
||||
|
||||
const logger = createLogger('Neo4jMergeAPI')
|
||||
|
||||
const MergeSchema = z.object({
|
||||
host: z.string().min(1, 'Host is required'),
|
||||
port: z.coerce.number().int().positive('Port must be a positive integer'),
|
||||
database: z.string().min(1, 'Database name is required'),
|
||||
username: z.string().min(1, 'Username is required'),
|
||||
password: z.string().min(1, 'Password is required'),
|
||||
encryption: z.enum(['enabled', 'disabled']).default('disabled'),
|
||||
cypherQuery: z.string().min(1, 'Cypher query is required'),
|
||||
parameters: z.record(z.unknown()).nullable().optional().default({}),
|
||||
})
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const requestId = randomUUID().slice(0, 8)
|
||||
let driver = null
|
||||
let session = null
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const params = MergeSchema.parse(body)
|
||||
|
||||
logger.info(
|
||||
`[${requestId}] Executing Neo4j merge on ${params.host}:${params.port}/${params.database}`
|
||||
)
|
||||
|
||||
const validation = validateCypherQuery(params.cypherQuery)
|
||||
if (!validation.isValid) {
|
||||
logger.warn(`[${requestId}] Cypher query validation failed: ${validation.error}`)
|
||||
return NextResponse.json(
|
||||
{ error: `Query validation failed: ${validation.error}` },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
driver = await createNeo4jDriver({
|
||||
host: params.host,
|
||||
port: params.port,
|
||||
database: params.database,
|
||||
username: params.username,
|
||||
password: params.password,
|
||||
encryption: params.encryption,
|
||||
})
|
||||
|
||||
session = driver.session({ database: params.database })
|
||||
|
||||
const result = await session.run(params.cypherQuery, params.parameters)
|
||||
|
||||
const records = result.records.map((record) => {
|
||||
const obj: Record<string, unknown> = {}
|
||||
record.keys.forEach((key) => {
|
||||
if (typeof key === 'string') {
|
||||
obj[key] = convertNeo4jTypesToJSON(record.get(key))
|
||||
}
|
||||
})
|
||||
return obj
|
||||
})
|
||||
|
||||
const summary = {
|
||||
resultAvailableAfter: result.summary.resultAvailableAfter.toNumber(),
|
||||
resultConsumedAfter: result.summary.resultConsumedAfter.toNumber(),
|
||||
counters: {
|
||||
nodesCreated: result.summary.counters.updates().nodesCreated,
|
||||
nodesDeleted: result.summary.counters.updates().nodesDeleted,
|
||||
relationshipsCreated: result.summary.counters.updates().relationshipsCreated,
|
||||
relationshipsDeleted: result.summary.counters.updates().relationshipsDeleted,
|
||||
propertiesSet: result.summary.counters.updates().propertiesSet,
|
||||
labelsAdded: result.summary.counters.updates().labelsAdded,
|
||||
labelsRemoved: result.summary.counters.updates().labelsRemoved,
|
||||
indexesAdded: result.summary.counters.updates().indexesAdded,
|
||||
indexesRemoved: result.summary.counters.updates().indexesRemoved,
|
||||
constraintsAdded: result.summary.counters.updates().constraintsAdded,
|
||||
constraintsRemoved: result.summary.counters.updates().constraintsRemoved,
|
||||
},
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`[${requestId}] Merge executed successfully, created ${summary.counters.nodesCreated} nodes, ${summary.counters.relationshipsCreated} relationships, returned ${records.length} records`
|
||||
)
|
||||
|
||||
return NextResponse.json({
|
||||
message: `Merge completed: ${summary.counters.nodesCreated} nodes created, ${summary.counters.relationshipsCreated} relationships created`,
|
||||
records,
|
||||
recordCount: records.length,
|
||||
summary,
|
||||
})
|
||||
} 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}] Neo4j merge failed:`, error)
|
||||
|
||||
return NextResponse.json({ error: `Neo4j merge failed: ${errorMessage}` }, { status: 500 })
|
||||
} finally {
|
||||
if (session) {
|
||||
await session.close()
|
||||
}
|
||||
if (driver) {
|
||||
await driver.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
116
apps/sim/app/api/tools/neo4j/query/route.ts
Normal file
116
apps/sim/app/api/tools/neo4j/query/route.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import { randomUUID } from 'crypto'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import {
|
||||
convertNeo4jTypesToJSON,
|
||||
createNeo4jDriver,
|
||||
validateCypherQuery,
|
||||
} from '@/app/api/tools/neo4j/utils'
|
||||
|
||||
const logger = createLogger('Neo4jQueryAPI')
|
||||
|
||||
const QuerySchema = z.object({
|
||||
host: z.string().min(1, 'Host is required'),
|
||||
port: z.coerce.number().int().positive('Port must be a positive integer'),
|
||||
database: z.string().min(1, 'Database name is required'),
|
||||
username: z.string().min(1, 'Username is required'),
|
||||
password: z.string().min(1, 'Password is required'),
|
||||
encryption: z.enum(['enabled', 'disabled']).default('disabled'),
|
||||
cypherQuery: z.string().min(1, 'Cypher query is required'),
|
||||
parameters: z.record(z.unknown()).nullable().optional().default({}),
|
||||
})
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const requestId = randomUUID().slice(0, 8)
|
||||
let driver = null
|
||||
let session = null
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const params = QuerySchema.parse(body)
|
||||
|
||||
logger.info(
|
||||
`[${requestId}] Executing Neo4j query on ${params.host}:${params.port}/${params.database}`
|
||||
)
|
||||
|
||||
const validation = validateCypherQuery(params.cypherQuery)
|
||||
if (!validation.isValid) {
|
||||
logger.warn(`[${requestId}] Cypher query validation failed: ${validation.error}`)
|
||||
return NextResponse.json(
|
||||
{ error: `Query validation failed: ${validation.error}` },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
driver = await createNeo4jDriver({
|
||||
host: params.host,
|
||||
port: params.port,
|
||||
database: params.database,
|
||||
username: params.username,
|
||||
password: params.password,
|
||||
encryption: params.encryption,
|
||||
})
|
||||
|
||||
session = driver.session({ database: params.database })
|
||||
|
||||
const result = await session.run(params.cypherQuery, params.parameters)
|
||||
|
||||
const records = result.records.map((record) => {
|
||||
const obj: Record<string, unknown> = {}
|
||||
record.keys.forEach((key) => {
|
||||
if (typeof key === 'string') {
|
||||
obj[key] = convertNeo4jTypesToJSON(record.get(key))
|
||||
}
|
||||
})
|
||||
return obj
|
||||
})
|
||||
|
||||
const summary = {
|
||||
resultAvailableAfter: result.summary.resultAvailableAfter.toNumber(),
|
||||
resultConsumedAfter: result.summary.resultConsumedAfter.toNumber(),
|
||||
counters: {
|
||||
nodesCreated: result.summary.counters.updates().nodesCreated,
|
||||
nodesDeleted: result.summary.counters.updates().nodesDeleted,
|
||||
relationshipsCreated: result.summary.counters.updates().relationshipsCreated,
|
||||
relationshipsDeleted: result.summary.counters.updates().relationshipsDeleted,
|
||||
propertiesSet: result.summary.counters.updates().propertiesSet,
|
||||
labelsAdded: result.summary.counters.updates().labelsAdded,
|
||||
labelsRemoved: result.summary.counters.updates().labelsRemoved,
|
||||
indexesAdded: result.summary.counters.updates().indexesAdded,
|
||||
indexesRemoved: result.summary.counters.updates().indexesRemoved,
|
||||
constraintsAdded: result.summary.counters.updates().constraintsAdded,
|
||||
constraintsRemoved: result.summary.counters.updates().constraintsRemoved,
|
||||
},
|
||||
}
|
||||
|
||||
logger.info(`[${requestId}] Query executed successfully, returned ${records.length} records`)
|
||||
|
||||
return NextResponse.json({
|
||||
message: `Found ${records.length} records`,
|
||||
records,
|
||||
recordCount: records.length,
|
||||
summary,
|
||||
})
|
||||
} 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}] Neo4j query failed:`, error)
|
||||
|
||||
return NextResponse.json({ error: `Neo4j query failed: ${errorMessage}` }, { status: 500 })
|
||||
} finally {
|
||||
if (session) {
|
||||
await session.close()
|
||||
}
|
||||
if (driver) {
|
||||
await driver.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
118
apps/sim/app/api/tools/neo4j/update/route.ts
Normal file
118
apps/sim/app/api/tools/neo4j/update/route.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import { randomUUID } from 'crypto'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import {
|
||||
convertNeo4jTypesToJSON,
|
||||
createNeo4jDriver,
|
||||
validateCypherQuery,
|
||||
} from '@/app/api/tools/neo4j/utils'
|
||||
|
||||
const logger = createLogger('Neo4jUpdateAPI')
|
||||
|
||||
const UpdateSchema = z.object({
|
||||
host: z.string().min(1, 'Host is required'),
|
||||
port: z.coerce.number().int().positive('Port must be a positive integer'),
|
||||
database: z.string().min(1, 'Database name is required'),
|
||||
username: z.string().min(1, 'Username is required'),
|
||||
password: z.string().min(1, 'Password is required'),
|
||||
encryption: z.enum(['enabled', 'disabled']).default('disabled'),
|
||||
cypherQuery: z.string().min(1, 'Cypher query is required'),
|
||||
parameters: z.record(z.unknown()).nullable().optional().default({}),
|
||||
})
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const requestId = randomUUID().slice(0, 8)
|
||||
let driver = null
|
||||
let session = null
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const params = UpdateSchema.parse(body)
|
||||
|
||||
logger.info(
|
||||
`[${requestId}] Executing Neo4j update on ${params.host}:${params.port}/${params.database}`
|
||||
)
|
||||
|
||||
const validation = validateCypherQuery(params.cypherQuery)
|
||||
if (!validation.isValid) {
|
||||
logger.warn(`[${requestId}] Cypher query validation failed: ${validation.error}`)
|
||||
return NextResponse.json(
|
||||
{ error: `Query validation failed: ${validation.error}` },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
driver = await createNeo4jDriver({
|
||||
host: params.host,
|
||||
port: params.port,
|
||||
database: params.database,
|
||||
username: params.username,
|
||||
password: params.password,
|
||||
encryption: params.encryption,
|
||||
})
|
||||
|
||||
session = driver.session({ database: params.database })
|
||||
|
||||
const result = await session.run(params.cypherQuery, params.parameters)
|
||||
|
||||
const records = result.records.map((record) => {
|
||||
const obj: Record<string, unknown> = {}
|
||||
record.keys.forEach((key) => {
|
||||
if (typeof key === 'string') {
|
||||
obj[key] = convertNeo4jTypesToJSON(record.get(key))
|
||||
}
|
||||
})
|
||||
return obj
|
||||
})
|
||||
|
||||
const summary = {
|
||||
resultAvailableAfter: result.summary.resultAvailableAfter.toNumber(),
|
||||
resultConsumedAfter: result.summary.resultConsumedAfter.toNumber(),
|
||||
counters: {
|
||||
nodesCreated: result.summary.counters.updates().nodesCreated,
|
||||
nodesDeleted: result.summary.counters.updates().nodesDeleted,
|
||||
relationshipsCreated: result.summary.counters.updates().relationshipsCreated,
|
||||
relationshipsDeleted: result.summary.counters.updates().relationshipsDeleted,
|
||||
propertiesSet: result.summary.counters.updates().propertiesSet,
|
||||
labelsAdded: result.summary.counters.updates().labelsAdded,
|
||||
labelsRemoved: result.summary.counters.updates().labelsRemoved,
|
||||
indexesAdded: result.summary.counters.updates().indexesAdded,
|
||||
indexesRemoved: result.summary.counters.updates().indexesRemoved,
|
||||
constraintsAdded: result.summary.counters.updates().constraintsAdded,
|
||||
constraintsRemoved: result.summary.counters.updates().constraintsRemoved,
|
||||
},
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`[${requestId}] Update executed successfully, ${summary.counters.propertiesSet} properties set, returned ${records.length} records`
|
||||
)
|
||||
|
||||
return NextResponse.json({
|
||||
message: `Updated ${summary.counters.propertiesSet} properties`,
|
||||
records,
|
||||
recordCount: records.length,
|
||||
summary,
|
||||
})
|
||||
} 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}] Neo4j update failed:`, error)
|
||||
|
||||
return NextResponse.json({ error: `Neo4j update failed: ${errorMessage}` }, { status: 500 })
|
||||
} finally {
|
||||
if (session) {
|
||||
await session.close()
|
||||
}
|
||||
if (driver) {
|
||||
await driver.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
163
apps/sim/app/api/tools/neo4j/utils.ts
Normal file
163
apps/sim/app/api/tools/neo4j/utils.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
import neo4j from 'neo4j-driver'
|
||||
import type { Neo4jConnectionConfig } from '@/tools/neo4j/types'
|
||||
|
||||
export async function createNeo4jDriver(config: Neo4jConnectionConfig) {
|
||||
const isAuraHost = config.host.includes('.databases.neo4j.io')
|
||||
|
||||
let protocol: string
|
||||
if (isAuraHost) {
|
||||
protocol = 'neo4j+s'
|
||||
} else {
|
||||
protocol = config.encryption === 'enabled' ? 'bolt+s' : 'bolt'
|
||||
}
|
||||
|
||||
const uri = `${protocol}://${config.host}:${config.port}`
|
||||
|
||||
const driverConfig: any = {
|
||||
maxConnectionPoolSize: 1,
|
||||
connectionTimeout: 10000,
|
||||
}
|
||||
|
||||
if (!protocol.endsWith('+s')) {
|
||||
driverConfig.encrypted = config.encryption === 'enabled' ? 'ENCRYPTION_ON' : 'ENCRYPTION_OFF'
|
||||
}
|
||||
|
||||
const driver = neo4j.driver(uri, neo4j.auth.basic(config.username, config.password), driverConfig)
|
||||
|
||||
await driver.verifyConnectivity()
|
||||
|
||||
return driver
|
||||
}
|
||||
|
||||
export function validateCypherQuery(
|
||||
query: string,
|
||||
allowDangerousOps = false
|
||||
): { isValid: boolean; error?: string } {
|
||||
if (!query || typeof query !== 'string') {
|
||||
return {
|
||||
isValid: false,
|
||||
error: 'Query must be a non-empty string',
|
||||
}
|
||||
}
|
||||
|
||||
if (!allowDangerousOps) {
|
||||
const dangerousPatterns = [
|
||||
/DROP\s+DATABASE/i,
|
||||
/DROP\s+CONSTRAINT/i,
|
||||
/DROP\s+INDEX/i,
|
||||
/CREATE\s+DATABASE/i,
|
||||
/CREATE\s+CONSTRAINT/i,
|
||||
/CREATE\s+INDEX/i,
|
||||
/CALL\s+dbms\./i,
|
||||
/CALL\s+db\./i,
|
||||
/LOAD\s+CSV/i,
|
||||
/apoc\.cypher\.run/i,
|
||||
/apoc\.load/i,
|
||||
/apoc\.periodic/i,
|
||||
]
|
||||
|
||||
for (const pattern of dangerousPatterns) {
|
||||
if (pattern.test(query)) {
|
||||
return {
|
||||
isValid: false,
|
||||
error:
|
||||
'Query contains potentially dangerous operations (schema changes, system procedures, or external data loading)',
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const trimmedQuery = query.trim()
|
||||
if (trimmedQuery.length === 0) {
|
||||
return {
|
||||
isValid: false,
|
||||
error: 'Query cannot be empty',
|
||||
}
|
||||
}
|
||||
|
||||
return { isValid: true }
|
||||
}
|
||||
|
||||
export function sanitizeLabelName(name: string): string {
|
||||
if (!/^[a-zA-Z][a-zA-Z0-9_]*$/.test(name)) {
|
||||
throw new Error(
|
||||
'Invalid label name. Must start with a letter and contain only letters, numbers, and underscores.'
|
||||
)
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
export function sanitizePropertyKey(key: string): string {
|
||||
if (!/^[a-zA-Z][a-zA-Z0-9_]*$/.test(key)) {
|
||||
throw new Error(
|
||||
'Invalid property key. Must start with a letter and contain only letters, numbers, and underscores.'
|
||||
)
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
export function sanitizeRelationshipType(type: string): string {
|
||||
if (!/^[A-Z][A-Z0-9_]*$/.test(type)) {
|
||||
throw new Error(
|
||||
'Invalid relationship type. Must start with an uppercase letter and contain only uppercase letters, numbers, and underscores.'
|
||||
)
|
||||
}
|
||||
return type
|
||||
}
|
||||
|
||||
export function convertNeo4jTypesToJSON(value: unknown): unknown {
|
||||
if (value === null || value === undefined) {
|
||||
return value
|
||||
}
|
||||
|
||||
if (typeof value === 'object' && value !== null && 'toNumber' in value) {
|
||||
return (value as any).toNumber()
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return value.map(convertNeo4jTypesToJSON)
|
||||
}
|
||||
|
||||
if (typeof value === 'object') {
|
||||
const obj = value as any
|
||||
|
||||
if (obj.labels && obj.properties && obj.identity) {
|
||||
return {
|
||||
identity: obj.identity.toNumber ? obj.identity.toNumber() : obj.identity,
|
||||
labels: obj.labels,
|
||||
properties: convertNeo4jTypesToJSON(obj.properties),
|
||||
}
|
||||
}
|
||||
|
||||
if (obj.type && obj.properties && obj.identity && obj.start && obj.end) {
|
||||
return {
|
||||
identity: obj.identity.toNumber ? obj.identity.toNumber() : obj.identity,
|
||||
start: obj.start.toNumber ? obj.start.toNumber() : obj.start,
|
||||
end: obj.end.toNumber ? obj.end.toNumber() : obj.end,
|
||||
type: obj.type,
|
||||
properties: convertNeo4jTypesToJSON(obj.properties),
|
||||
}
|
||||
}
|
||||
|
||||
if (obj.start && obj.end && obj.segments) {
|
||||
return {
|
||||
start: convertNeo4jTypesToJSON(obj.start),
|
||||
end: convertNeo4jTypesToJSON(obj.end),
|
||||
segments: obj.segments.map((seg: any) => ({
|
||||
start: convertNeo4jTypesToJSON(seg.start),
|
||||
relationship: convertNeo4jTypesToJSON(seg.relationship),
|
||||
end: convertNeo4jTypesToJSON(seg.end),
|
||||
})),
|
||||
length: obj.length,
|
||||
}
|
||||
}
|
||||
|
||||
const result: Record<string, unknown> = {}
|
||||
for (const [key, val] of Object.entries(obj)) {
|
||||
result[key] = convertNeo4jTypesToJSON(val)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
@@ -297,6 +297,31 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
}
|
||||
|
||||
if (provider === 'calendly') {
|
||||
logger.info(`[${requestId}] Creating Calendly subscription before saving to database`)
|
||||
try {
|
||||
externalSubscriptionId = await createCalendlyWebhookSubscription(
|
||||
request,
|
||||
userId,
|
||||
createTempWebhookData(),
|
||||
requestId
|
||||
)
|
||||
if (externalSubscriptionId) {
|
||||
resolvedProviderConfig.externalId = externalSubscriptionId
|
||||
externalSubscriptionCreated = true
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(`[${requestId}] Error creating Calendly webhook subscription`, err)
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Failed to create webhook in Calendly',
|
||||
details: err instanceof Error ? err.message : 'Unknown error',
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (provider === 'microsoft-teams') {
|
||||
const { createTeamsSubscription } = await import('@/lib/webhooks/webhook-helpers')
|
||||
logger.info(`[${requestId}] Creating Teams subscription before saving to database`)
|
||||
@@ -635,6 +660,140 @@ async function createAirtableWebhookSubscription(
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to create the webhook subscription in Calendly
|
||||
async function createCalendlyWebhookSubscription(
|
||||
request: NextRequest,
|
||||
userId: string,
|
||||
webhookData: any,
|
||||
requestId: string
|
||||
): Promise<string | undefined> {
|
||||
try {
|
||||
const { path, providerConfig } = webhookData
|
||||
const { apiKey, organization, triggerId } = providerConfig || {}
|
||||
|
||||
if (!apiKey) {
|
||||
logger.warn(`[${requestId}] Missing apiKey for Calendly webhook creation.`, {
|
||||
webhookId: webhookData.id,
|
||||
})
|
||||
throw new Error(
|
||||
'Personal Access Token is required to create Calendly webhook. Please provide your Calendly Personal Access Token.'
|
||||
)
|
||||
}
|
||||
|
||||
if (!organization) {
|
||||
logger.warn(`[${requestId}] Missing organization URI for Calendly webhook creation.`, {
|
||||
webhookId: webhookData.id,
|
||||
})
|
||||
throw new Error(
|
||||
'Organization URI is required to create Calendly webhook. Please provide your Organization URI from the "Get Current User" operation.'
|
||||
)
|
||||
}
|
||||
|
||||
if (!triggerId) {
|
||||
logger.warn(`[${requestId}] Missing triggerId for Calendly webhook creation.`, {
|
||||
webhookId: webhookData.id,
|
||||
})
|
||||
throw new Error('Trigger ID is required to create Calendly webhook')
|
||||
}
|
||||
|
||||
const notificationUrl = `${getBaseUrl()}/api/webhooks/trigger/${path}`
|
||||
|
||||
// Map trigger IDs to Calendly event types
|
||||
const eventTypeMap: Record<string, string[]> = {
|
||||
calendly_invitee_created: ['invitee.created'],
|
||||
calendly_invitee_canceled: ['invitee.canceled'],
|
||||
calendly_routing_form_submitted: ['routing_form_submission.created'],
|
||||
calendly_webhook: ['invitee.created', 'invitee.canceled', 'routing_form_submission.created'],
|
||||
}
|
||||
|
||||
const events = eventTypeMap[triggerId] || ['invitee.created']
|
||||
|
||||
const calendlyApiUrl = 'https://api.calendly.com/webhook_subscriptions'
|
||||
|
||||
const requestBody = {
|
||||
url: notificationUrl,
|
||||
events,
|
||||
organization,
|
||||
scope: 'organization',
|
||||
}
|
||||
|
||||
const calendlyResponse = await fetch(calendlyApiUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(requestBody),
|
||||
})
|
||||
|
||||
if (!calendlyResponse.ok) {
|
||||
const errorBody = await calendlyResponse.json().catch(() => ({}))
|
||||
const errorMessage = errorBody.message || errorBody.title || 'Unknown Calendly API error'
|
||||
logger.error(
|
||||
`[${requestId}] Failed to create webhook in Calendly for webhook ${webhookData.id}. Status: ${calendlyResponse.status}`,
|
||||
{ response: errorBody }
|
||||
)
|
||||
|
||||
let userFriendlyMessage = 'Failed to create webhook subscription in Calendly'
|
||||
if (calendlyResponse.status === 401) {
|
||||
userFriendlyMessage =
|
||||
'Calendly authentication failed. Please verify your Personal Access Token is correct.'
|
||||
} else if (calendlyResponse.status === 403) {
|
||||
userFriendlyMessage =
|
||||
'Calendly access denied. Please ensure you have appropriate permissions and a paid Calendly subscription.'
|
||||
} else if (calendlyResponse.status === 404) {
|
||||
userFriendlyMessage =
|
||||
'Calendly organization not found. Please verify the Organization URI is correct.'
|
||||
} else if (errorMessage && errorMessage !== 'Unknown Calendly API error') {
|
||||
userFriendlyMessage = `Calendly error: ${errorMessage}`
|
||||
}
|
||||
|
||||
throw new Error(userFriendlyMessage)
|
||||
}
|
||||
|
||||
const responseBody = await calendlyResponse.json()
|
||||
const webhookUri = responseBody.resource?.uri
|
||||
|
||||
if (!webhookUri) {
|
||||
logger.error(
|
||||
`[${requestId}] Calendly webhook created but no webhook URI returned for webhook ${webhookData.id}`,
|
||||
{ response: responseBody }
|
||||
)
|
||||
throw new Error('Calendly webhook creation succeeded but no webhook URI was returned')
|
||||
}
|
||||
|
||||
// Extract the webhook ID from the URI (e.g., https://api.calendly.com/webhook_subscriptions/WEBHOOK_ID)
|
||||
const webhookId = webhookUri.split('/').pop()
|
||||
|
||||
if (!webhookId) {
|
||||
logger.error(`[${requestId}] Could not extract webhook ID from Calendly URI: ${webhookUri}`, {
|
||||
response: responseBody,
|
||||
})
|
||||
throw new Error('Failed to extract webhook ID from Calendly response')
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`[${requestId}] Successfully created webhook in Calendly for webhook ${webhookData.id}.`,
|
||||
{
|
||||
calendlyWebhookUri: webhookUri,
|
||||
calendlyWebhookId: webhookId,
|
||||
}
|
||||
)
|
||||
return webhookId
|
||||
} catch (error: any) {
|
||||
logger.error(
|
||||
`[${requestId}] Exception during Calendly webhook creation for webhook ${webhookData.id}.`,
|
||||
{
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
}
|
||||
)
|
||||
// Re-throw the error so it can be caught by the outer try-catch
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to create the webhook subscription in Webflow
|
||||
async function createWebflowWebhookSubscription(
|
||||
request: NextRequest,
|
||||
|
||||
@@ -577,10 +577,6 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
||||
if (isStreamClosed) return
|
||||
|
||||
try {
|
||||
logger.info(`[${requestId}] 📤 Sending SSE event:`, {
|
||||
type: event.type,
|
||||
data: event.data,
|
||||
})
|
||||
controller.enqueue(encodeSSEEvent(event))
|
||||
} catch {
|
||||
isStreamClosed = true
|
||||
|
||||
@@ -359,7 +359,7 @@ export function OutputSelect({
|
||||
<div ref={triggerRef} className='min-w-0 max-w-full'>
|
||||
<Badge
|
||||
variant='outline'
|
||||
className='min-w-0 max-w-full cursor-pointer rounded-[6px]'
|
||||
className='min-w-0 max-w-full cursor-pointer rounded-[4px] border-[var(--surface-11)] bg-[var(--surface-6)] dark:bg-[var(--surface-9)]'
|
||||
title='Select outputs'
|
||||
aria-expanded={open}
|
||||
onMouseDown={(e) => {
|
||||
@@ -378,7 +378,7 @@ export function OutputSelect({
|
||||
align={align}
|
||||
sideOffset={4}
|
||||
maxHeight={maxHeight}
|
||||
maxWidth={160}
|
||||
maxWidth={300}
|
||||
minWidth={160}
|
||||
disablePortal={disablePopoverPortal}
|
||||
onKeyDown={handleKeyDown}
|
||||
|
||||
@@ -2,23 +2,19 @@
|
||||
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { AlertTriangle, Loader2 } from 'lucide-react'
|
||||
import { Input, Label, Textarea } from '@/components/emcn'
|
||||
import {
|
||||
Alert,
|
||||
AlertDescription,
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
Card,
|
||||
CardContent,
|
||||
ImageUpload,
|
||||
Skeleton,
|
||||
} from '@/components/ui'
|
||||
Button,
|
||||
Input,
|
||||
Label,
|
||||
Modal,
|
||||
ModalContent,
|
||||
ModalDescription,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
ModalTitle,
|
||||
Textarea,
|
||||
} from '@/components/emcn'
|
||||
import { Alert, AlertDescription, Skeleton } from '@/components/ui'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { getEmailDomain } from '@/lib/urls/utils'
|
||||
import { OutputSelect } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/chat/components/output-select/output-select'
|
||||
@@ -223,10 +219,6 @@ export function ChatDeploy({
|
||||
throw new Error(error.error || 'Failed to delete chat')
|
||||
}
|
||||
|
||||
if (onUndeploy) {
|
||||
await onUndeploy()
|
||||
}
|
||||
|
||||
setExistingChat(null)
|
||||
setImageUrl(null)
|
||||
setImageUploadError(null)
|
||||
@@ -259,42 +251,42 @@ export function ChatDeploy({
|
||||
</div>
|
||||
|
||||
{/* Delete Confirmation Dialog */}
|
||||
<AlertDialog open={showDeleteConfirmation} onOpenChange={setShowDeleteConfirmation}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Delete Chat?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This will permanently delete your chat deployment at{' '}
|
||||
<span className='font-mono text-destructive'>
|
||||
{getEmailDomain()}/chat/{existingChat?.identifier}
|
||||
</span>{' '}
|
||||
and undeploy the workflow.
|
||||
<span className='mt-2 block'>
|
||||
All users will lose access immediately, and this action cannot be undone.
|
||||
</span>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter className='flex'>
|
||||
<AlertDialogCancel className='h-9 w-full rounded-[8px]' disabled={isDeleting}>
|
||||
<Modal open={showDeleteConfirmation} onOpenChange={setShowDeleteConfirmation}>
|
||||
<ModalContent>
|
||||
<ModalHeader>
|
||||
<ModalTitle>Delete Chat?</ModalTitle>
|
||||
<ModalDescription>
|
||||
This will delete your chat deployment at "{getEmailDomain()}/chat/
|
||||
{existingChat?.identifier}". All users will lose access to the chat interface. You
|
||||
can recreate this chat deployment at any time.
|
||||
</ModalDescription>
|
||||
</ModalHeader>
|
||||
<ModalFooter>
|
||||
<Button
|
||||
variant='outline'
|
||||
className='h-[32px] px-[12px]'
|
||||
onClick={() => setShowDeleteConfirmation(false)}
|
||||
disabled={isDeleting}
|
||||
>
|
||||
Cancel
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleDelete}
|
||||
disabled={isDeleting}
|
||||
className='h-9 w-full rounded-[8px] bg-red-500 text-white transition-all duration-200 hover:bg-red-600 dark:bg-red-500 dark:hover:bg-red-600'
|
||||
className='h-[32px] bg-[var(--text-error)] px-[12px] text-[var(--white)] hover:bg-[var(--text-error)] hover:text-[var(--white)] dark:bg-[var(--text-error)] dark:text-[var(--white)] hover:dark:bg-[var(--text-error)] dark:hover:text-[var(--white)]'
|
||||
>
|
||||
{isDeleting ? (
|
||||
<span className='flex items-center'>
|
||||
<Loader2 className='mr-2 h-4 w-4 animate-spin' />
|
||||
<>
|
||||
<Loader2 className='mr-2 h-3.5 w-3.5 animate-spin' />
|
||||
Deleting...
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
'Delete'
|
||||
)}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -335,13 +327,12 @@ export function ChatDeploy({
|
||||
onChange={(e) => updateField('title', e.target.value)}
|
||||
required
|
||||
disabled={chatSubmitting}
|
||||
className='h-10 rounded-[8px]'
|
||||
/>
|
||||
{errors.title && <p className='text-destructive text-sm'>{errors.title}</p>}
|
||||
</div>
|
||||
<div className='space-y-2'>
|
||||
<Label htmlFor='description' className='font-medium text-sm'>
|
||||
Description (Optional)
|
||||
Description
|
||||
</Label>
|
||||
<Textarea
|
||||
id='description'
|
||||
@@ -350,13 +341,11 @@ export function ChatDeploy({
|
||||
onChange={(e) => updateField('description', e.target.value)}
|
||||
rows={3}
|
||||
disabled={chatSubmitting}
|
||||
className='min-h-[80px] resize-none rounded-[8px]'
|
||||
className='min-h-[80px] resize-none'
|
||||
/>
|
||||
</div>
|
||||
<div className='space-y-2'>
|
||||
<Label className='font-medium text-sm'>Chat Output</Label>
|
||||
<Card className='rounded-[8px] border-input shadow-none'>
|
||||
<CardContent className='p-1'>
|
||||
<OutputSelect
|
||||
workflowId={workflowId}
|
||||
selectedOutputs={formData.selectedOutputBlocks}
|
||||
@@ -364,12 +353,10 @@ export function ChatDeploy({
|
||||
placeholder='Select which block outputs to use'
|
||||
disabled={chatSubmitting}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
{errors.outputBlocks && (
|
||||
<p className='text-destructive text-sm'>{errors.outputBlocks}</p>
|
||||
)}
|
||||
<p className='mt-2 text-muted-foreground text-xs'>
|
||||
<p className='mt-2 text-[11px] text-[var(--text-secondary)]'>
|
||||
Select which block's output to return to the user in the chat interface
|
||||
</p>
|
||||
</div>
|
||||
@@ -396,14 +383,14 @@ export function ChatDeploy({
|
||||
onChange={(e) => updateField('welcomeMessage', e.target.value)}
|
||||
rows={3}
|
||||
disabled={chatSubmitting}
|
||||
className='min-h-[80px] resize-none rounded-[8px]'
|
||||
className='min-h-[80px] resize-none'
|
||||
/>
|
||||
<p className='text-muted-foreground text-xs'>
|
||||
This message will be displayed when users first open the chat
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className='space-y-2'>
|
||||
{/* <div className='space-y-2'>
|
||||
<Label className='font-medium text-sm'>Chat Logo</Label>
|
||||
<ImageUpload
|
||||
value={imageUrl}
|
||||
@@ -424,7 +411,7 @@ export function ChatDeploy({
|
||||
Upload a logo for your chat (PNG, JPEG - max 5MB)
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div> */}
|
||||
|
||||
<button
|
||||
type='button'
|
||||
@@ -435,42 +422,42 @@ export function ChatDeploy({
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<AlertDialog open={showDeleteConfirmation} onOpenChange={setShowDeleteConfirmation}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Delete Chat?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This will permanently delete your chat deployment at{' '}
|
||||
<span className='font-mono text-destructive'>
|
||||
{getEmailDomain()}/chat/{existingChat?.identifier}
|
||||
</span>{' '}
|
||||
and undeploy the workflow.
|
||||
<span className='mt-2 block'>
|
||||
All users will lose access immediately, and this action cannot be undone.
|
||||
</span>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter className='flex'>
|
||||
<AlertDialogCancel className='h-9 w-full rounded-[8px]' disabled={isDeleting}>
|
||||
<Modal open={showDeleteConfirmation} onOpenChange={setShowDeleteConfirmation}>
|
||||
<ModalContent>
|
||||
<ModalHeader>
|
||||
<ModalTitle>Delete Chat?</ModalTitle>
|
||||
<ModalDescription>
|
||||
This will delete your chat deployment at "{getEmailDomain()}/chat/
|
||||
{existingChat?.identifier}". All users will lose access to the chat interface. You can
|
||||
recreate this chat deployment at any time.
|
||||
</ModalDescription>
|
||||
</ModalHeader>
|
||||
<ModalFooter>
|
||||
<Button
|
||||
variant='outline'
|
||||
className='h-[32px] px-[12px]'
|
||||
onClick={() => setShowDeleteConfirmation(false)}
|
||||
disabled={isDeleting}
|
||||
>
|
||||
Cancel
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleDelete}
|
||||
disabled={isDeleting}
|
||||
className='h-9 w-full rounded-[8px] bg-red-500 text-white transition-all duration-200 hover:bg-red-600 dark:bg-red-500 dark:hover:bg-red-600'
|
||||
className='h-[32px] bg-[var(--text-error)] px-[12px] text-[var(--white)] hover:bg-[var(--text-error)] hover:text-[var(--white)] dark:bg-[var(--text-error)] dark:text-[var(--white)] hover:dark:bg-[var(--text-error)] dark:hover:text-[var(--white)]'
|
||||
>
|
||||
{isDeleting ? (
|
||||
<span className='flex items-center'>
|
||||
<Loader2 className='mr-2 h-4 w-4 animate-spin' />
|
||||
<>
|
||||
<Loader2 className='mr-2 h-3.5 w-3.5 animate-spin' />
|
||||
Deleting...
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
'Delete'
|
||||
)}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useState } from 'react'
|
||||
import { Check, Copy, Eye, EyeOff, Plus, RefreshCw } from 'lucide-react'
|
||||
import { Input, Label } from '@/components/emcn'
|
||||
import { Button, Input, Label } from '@/components/emcn'
|
||||
import { Trash } from '@/components/emcn/icons/trash'
|
||||
import { Button, Card, CardContent } from '@/components/ui'
|
||||
import { Card, CardContent } from '@/components/ui'
|
||||
import { getEnv, isTruthy } from '@/lib/env'
|
||||
import { cn, generatePassword } from '@/lib/utils'
|
||||
import type { AuthType } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/components/chat-deploy/hooks/use-chat-form'
|
||||
@@ -83,13 +83,13 @@ export function AuthSelector({
|
||||
<Card
|
||||
key={type}
|
||||
className={cn(
|
||||
'cursor-pointer overflow-hidden rounded-[8px] shadow-none transition-all duration-200 hover:bg-accent/30',
|
||||
'cursor-pointer overflow-hidden rounded-[4px] shadow-none transition-all duration-200',
|
||||
authType === type
|
||||
? 'border border-muted-foreground hover:bg-accent/50'
|
||||
: 'border border-input'
|
||||
? 'border border-[#727272] bg-[var(--border-strong)] dark:border-[#727272] dark:bg-[var(--border-strong)]'
|
||||
: 'border border-[var(--surface-11)] bg-[var(--surface-6)] hover:bg-[var(--surface-9)] dark:bg-[var(--surface-9)] dark:hover:bg-[var(--surface-11)]'
|
||||
)}
|
||||
>
|
||||
<CardContent className='relative flex flex-col items-center justify-center p-4 text-center'>
|
||||
<CardContent className='relative flex flex-col items-center justify-center p-3 text-center'>
|
||||
<button
|
||||
type='button'
|
||||
className='absolute inset-0 z-10 h-full w-full cursor-pointer'
|
||||
@@ -98,13 +98,18 @@ export function AuthSelector({
|
||||
disabled={disabled}
|
||||
/>
|
||||
<div className='justify-center text-center align-middle'>
|
||||
<h3 className='font-medium text-sm'>
|
||||
<h3
|
||||
className={cn(
|
||||
'font-medium text-xs',
|
||||
authType === type && 'text-[var(--text-primary)]'
|
||||
)}
|
||||
>
|
||||
{type === 'public' && 'Public Access'}
|
||||
{type === 'password' && 'Password Protected'}
|
||||
{type === 'email' && 'Email Access'}
|
||||
{type === 'sso' && 'SSO Access'}
|
||||
</h3>
|
||||
<p className='text-muted-foreground text-xs'>
|
||||
<p className='text-[11px] text-[var(--text-tertiary)]'>
|
||||
{type === 'public' && 'Anyone can access your chat'}
|
||||
{type === 'password' && 'Secure with a single password'}
|
||||
{type === 'email' && 'Restrict to specific emails'}
|
||||
@@ -118,13 +123,15 @@ export function AuthSelector({
|
||||
|
||||
{/* Auth Settings */}
|
||||
{authType === 'password' && (
|
||||
<Card className='rounded-[8px] shadow-none'>
|
||||
<Card className='rounded-[4px] border-[var(--surface-11)] bg-[var(--surface-6)] shadow-none dark:bg-[var(--surface-9)]'>
|
||||
<CardContent className='p-4'>
|
||||
<h3 className='mb-2 font-medium text-sm'>Password Settings</h3>
|
||||
<h3 className='mb-2 font-medium text-[var(--text-primary)] text-xs'>
|
||||
Password Settings
|
||||
</h3>
|
||||
|
||||
{isExistingChat && !password && (
|
||||
<div className='mb-2 flex items-center text-muted-foreground text-xs'>
|
||||
<div className='mr-2 rounded-full bg-primary/10 px-2 py-0.5 font-medium text-muted-foreground'>
|
||||
<div className='mb-2 flex items-center text-[11px] text-[var(--text-secondary)]'>
|
||||
<div className='mr-2 rounded-full bg-[var(--surface-9)] px-2 py-0.5 font-medium text-[var(--text-secondary)] dark:bg-[var(--surface-11)]'>
|
||||
Password set
|
||||
</div>
|
||||
<span>Current password is securely stored</span>
|
||||
@@ -142,66 +149,46 @@ export function AuthSelector({
|
||||
value={password}
|
||||
onChange={(e) => onPasswordChange(e.target.value)}
|
||||
disabled={disabled}
|
||||
className='h-10 rounded-[8px] pr-32'
|
||||
className='pr-28'
|
||||
required={!isExistingChat}
|
||||
autoComplete='new-password'
|
||||
/>
|
||||
<div className='absolute top-0.5 right-0.5 flex h-9 items-center gap-1 pr-1'>
|
||||
<div className='-translate-y-1/2 absolute top-1/2 right-1 flex items-center gap-1'>
|
||||
<Button
|
||||
type='button'
|
||||
variant='ghost'
|
||||
size='sm'
|
||||
onClick={handleGeneratePassword}
|
||||
disabled={disabled}
|
||||
className={cn(
|
||||
'group h-7 w-7 rounded-md p-0',
|
||||
'text-muted-foreground/60 transition-all duration-200',
|
||||
'hover:bg-muted/50 hover:text-foreground',
|
||||
'disabled:cursor-not-allowed disabled:opacity-50',
|
||||
'focus-visible:ring-2 focus-visible:ring-muted-foreground/20 focus-visible:ring-offset-1'
|
||||
)}
|
||||
className='h-6 w-6 p-0'
|
||||
>
|
||||
<RefreshCw className='h-3.5 w-3.5 transition-transform duration-200 group-hover:rotate-90' />
|
||||
<RefreshCw className='h-3.5 w-3.5 transition-transform duration-200 hover:rotate-90' />
|
||||
<span className='sr-only'>Generate password</span>
|
||||
</Button>
|
||||
<Button
|
||||
type='button'
|
||||
variant='ghost'
|
||||
size='sm'
|
||||
onClick={() => copyToClipboard(password)}
|
||||
disabled={!password || disabled}
|
||||
className={cn(
|
||||
'group h-7 w-7 rounded-md p-0',
|
||||
'text-muted-foreground/60 transition-all duration-200',
|
||||
'hover:bg-muted/50 hover:text-foreground',
|
||||
'disabled:cursor-not-allowed disabled:opacity-30',
|
||||
'focus-visible:ring-2 focus-visible:ring-muted-foreground/20 focus-visible:ring-offset-1'
|
||||
)}
|
||||
className='h-6 w-6 p-0'
|
||||
>
|
||||
{copySuccess ? (
|
||||
<Check className='h-3.5 w-3.5 text-foreground' />
|
||||
<Check className='h-3.5 w-3.5' />
|
||||
) : (
|
||||
<Copy className='h-3.5 w-3.5 ' />
|
||||
<Copy className='h-3.5 w-3.5' />
|
||||
)}
|
||||
<span className='sr-only'>Copy password</span>
|
||||
</Button>
|
||||
<Button
|
||||
type='button'
|
||||
variant='ghost'
|
||||
size='sm'
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
disabled={disabled}
|
||||
className={cn(
|
||||
'group h-7 w-7 rounded-md p-0',
|
||||
'text-muted-foreground/60 transition-all duration-200',
|
||||
'hover:bg-muted/50 hover:text-foreground',
|
||||
'focus-visible:ring-2 focus-visible:ring-muted-foreground/20 focus-visible:ring-offset-1'
|
||||
)}
|
||||
className='h-6 w-6 p-0'
|
||||
>
|
||||
{showPassword ? (
|
||||
<EyeOff className='h-3.5 w-3.5 ' />
|
||||
<EyeOff className='h-3.5 w-3.5' />
|
||||
) : (
|
||||
<Eye className='h-3.5 w-3.5 ' />
|
||||
<Eye className='h-3.5 w-3.5' />
|
||||
)}
|
||||
<span className='sr-only'>
|
||||
{showPassword ? 'Hide password' : 'Show password'}
|
||||
@@ -210,7 +197,7 @@ export function AuthSelector({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className='mt-2 text-muted-foreground text-xs'>
|
||||
<p className='mt-2 text-[11px] text-[var(--text-secondary)]'>
|
||||
{isExistingChat
|
||||
? 'Leaving this empty will keep the current password. Enter a new password to change it.'
|
||||
: 'This password will be required to access your chat.'}
|
||||
@@ -220,9 +207,9 @@ export function AuthSelector({
|
||||
)}
|
||||
|
||||
{(authType === 'email' || authType === 'sso') && (
|
||||
<Card className='rounded-[8px] shadow-none'>
|
||||
<Card className='rounded-[4px] border-[var(--surface-11)] bg-[var(--surface-6)] shadow-none dark:bg-[var(--surface-9)]'>
|
||||
<CardContent className='p-4'>
|
||||
<h3 className='mb-2 font-medium text-sm'>
|
||||
<h3 className='mb-2 font-medium text-[var(--text-primary)] text-xs'>
|
||||
{authType === 'email' ? 'Email Access Settings' : 'SSO Access Settings'}
|
||||
</h3>
|
||||
|
||||
@@ -232,7 +219,7 @@ export function AuthSelector({
|
||||
value={newEmail}
|
||||
onChange={(e) => setNewEmail(e.target.value)}
|
||||
disabled={disabled}
|
||||
className='h-10 flex-1 rounded-[8px]'
|
||||
className='flex-1'
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault()
|
||||
@@ -242,9 +229,10 @@ export function AuthSelector({
|
||||
/>
|
||||
<Button
|
||||
type='button'
|
||||
variant='default'
|
||||
onClick={handleAddEmail}
|
||||
disabled={!newEmail.trim() || disabled}
|
||||
className='h-10 shrink-0 rounded-[8px]'
|
||||
className='shrink-0 gap-[4px]'
|
||||
>
|
||||
<Plus className='h-4 w-4' />
|
||||
Add
|
||||
@@ -263,10 +251,9 @@ export function AuthSelector({
|
||||
<Button
|
||||
type='button'
|
||||
variant='ghost'
|
||||
size='icon'
|
||||
onClick={() => handleRemoveEmail(email)}
|
||||
disabled={disabled}
|
||||
className='h-7 w-7 opacity-70'
|
||||
className='h-7 w-7 p-0 opacity-70'
|
||||
>
|
||||
<Trash className='h-4 w-4' />
|
||||
</Button>
|
||||
@@ -277,7 +264,7 @@ export function AuthSelector({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p className='mt-2 text-muted-foreground text-xs'>
|
||||
<p className='mt-2 text-[11px] text-[var(--text-secondary)]'>
|
||||
{authType === 'email'
|
||||
? 'Add specific emails or entire domains (@example.com)'
|
||||
: 'Add specific emails or entire domains (@example.com) that can access via SSO'}
|
||||
@@ -287,10 +274,12 @@ export function AuthSelector({
|
||||
)}
|
||||
|
||||
{authType === 'public' && (
|
||||
<Card className='rounded-[8px] shadow-none'>
|
||||
<Card className='rounded-[4px] border-[var(--surface-11)] bg-[var(--surface-6)] shadow-none dark:bg-[var(--surface-9)]'>
|
||||
<CardContent className='p-4'>
|
||||
<h3 className='mb-2 font-medium text-sm'>Public Access Settings</h3>
|
||||
<p className='text-muted-foreground text-xs'>
|
||||
<h3 className='mb-2 font-medium text-[var(--text-primary)] text-xs'>
|
||||
Public Access Settings
|
||||
</h3>
|
||||
<p className='text-[11px] text-[var(--text-secondary)]'>
|
||||
This chat will be publicly accessible to anyone with the link.
|
||||
</p>
|
||||
</CardContent>
|
||||
|
||||
@@ -47,8 +47,8 @@ export function IdentifierInput({
|
||||
<Label htmlFor='identifier' className='font-medium text-sm'>
|
||||
Identifier
|
||||
</Label>
|
||||
<div className='relative flex items-center rounded-md ring-offset-background focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2'>
|
||||
<div className='flex h-10 items-center whitespace-nowrap rounded-l-md border border-r-0 bg-muted px-3 font-medium text-muted-foreground text-sm'>
|
||||
<div className='relative flex items-stretch'>
|
||||
<div className='flex items-center whitespace-nowrap rounded-l-[4px] border border-[var(--surface-11)] border-r-0 bg-[var(--surface-6)] px-[8px] py-[6px] font-medium text-[var(--text-secondary)] text-sm dark:bg-[var(--surface-9)]'>
|
||||
{getDomainPrefix()}
|
||||
</div>
|
||||
<div className='relative flex-1'>
|
||||
@@ -60,9 +60,9 @@ export function IdentifierInput({
|
||||
required
|
||||
disabled={disabled}
|
||||
className={cn(
|
||||
'rounded-l-none border-l-0 focus-visible:ring-0 focus-visible:ring-offset-0',
|
||||
'rounded-l-none border-l-0',
|
||||
isChecking && 'pr-8',
|
||||
error && 'border-destructive focus-visible:border-destructive'
|
||||
error && 'border-destructive'
|
||||
)}
|
||||
/>
|
||||
{isChecking && (
|
||||
|
||||
@@ -9,7 +9,7 @@ interface DeployStatusProps {
|
||||
export function DeployStatus({ needsRedeployment }: DeployStatusProps) {
|
||||
return (
|
||||
<div className='flex items-center gap-2'>
|
||||
<span className='font-medium text-muted-foreground text-xs'>Status:</span>
|
||||
<span className='font-medium text-[var(--text-secondary)] text-xs'>Status:</span>
|
||||
<div className='flex items-center gap-1.5'>
|
||||
<div className='relative flex items-center justify-center'>
|
||||
{needsRedeployment ? (
|
||||
@@ -28,8 +28,8 @@ export function DeployStatus({ needsRedeployment }: DeployStatusProps) {
|
||||
className={cn(
|
||||
'font-medium text-xs',
|
||||
needsRedeployment
|
||||
? 'bg-amber-50 text-amber-600 dark:bg-amber-900/20 dark:text-amber-400'
|
||||
: 'bg-green-50 text-green-600 dark:bg-green-900/20 dark:text-green-400'
|
||||
? 'text-amber-600 dark:text-amber-400'
|
||||
: 'text-green-600 dark:text-green-400'
|
||||
)}
|
||||
>
|
||||
{needsRedeployment ? 'Changes Detected' : 'Active'}
|
||||
|
||||
@@ -147,14 +147,14 @@ export function ExampleCommand({
|
||||
{showLabel && <Label className='font-medium text-sm'>Example</Label>}
|
||||
<div className='flex items-center gap-1'>
|
||||
<Button
|
||||
variant={mode === 'sync' ? 'primary' : 'outline'}
|
||||
variant={mode === 'sync' ? 'active' : 'default'}
|
||||
onClick={() => setMode('sync')}
|
||||
className='h-6 min-w-[50px] px-2 py-1 text-xs'
|
||||
>
|
||||
Sync
|
||||
</Button>
|
||||
<Button
|
||||
variant={mode === 'stream' ? 'primary' : 'outline'}
|
||||
variant={mode === 'stream' ? 'active' : 'default'}
|
||||
onClick={() => setMode('stream')}
|
||||
className='h-6 min-w-[50px] px-2 py-1 text-xs'
|
||||
>
|
||||
@@ -163,7 +163,7 @@ export function ExampleCommand({
|
||||
{isAsyncEnabled && (
|
||||
<>
|
||||
<Button
|
||||
variant={mode === 'async' ? 'primary' : 'outline'}
|
||||
variant={mode === 'async' ? 'active' : 'default'}
|
||||
onClick={() => setMode('async')}
|
||||
className='h-6 min-w-[50px] px-2 py-1 text-xs'
|
||||
>
|
||||
|
||||
@@ -133,7 +133,7 @@ export function DeploymentInfo({
|
||||
</Button>
|
||||
{deploymentInfo.needsRedeployment && (
|
||||
<Button
|
||||
variant='outline'
|
||||
variant='primary'
|
||||
onClick={onRedeploy}
|
||||
disabled={isSubmitting}
|
||||
className='h-8 text-xs'
|
||||
@@ -172,10 +172,9 @@ export function DeploymentInfo({
|
||||
<ModalHeader>
|
||||
<ModalTitle>Undeploy API</ModalTitle>
|
||||
<ModalDescription>
|
||||
Are you sure you want to undeploy this workflow? This will remove the API endpoint and
|
||||
make it unavailable to external users.{' '}
|
||||
Are you sure you want to undeploy this workflow?{' '}
|
||||
<span className='text-[var(--text-error)] dark:text-[var(--text-error)]'>
|
||||
This action cannot be undone.
|
||||
This will remove the API endpoint and make it unavailable to external users.{' '}
|
||||
</span>
|
||||
</ModalDescription>
|
||||
</ModalHeader>
|
||||
|
||||
@@ -2,18 +2,15 @@
|
||||
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { Loader2, MoreVertical, X } from 'lucide-react'
|
||||
import { Badge, Button } from '@/components/emcn'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
Button as UIButton,
|
||||
} from '@/components/ui'
|
||||
Badge,
|
||||
Button,
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverItem,
|
||||
PopoverTrigger,
|
||||
} from '@/components/emcn'
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui'
|
||||
import { getEnv } from '@/lib/env'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import type { WorkflowDeploymentVersionResponse } from '@/lib/workflows/db-helpers'
|
||||
@@ -658,10 +655,7 @@ export function DeployModal({
|
||||
<div className='flex items-center gap-2'>
|
||||
<DialogTitle className='font-medium text-lg'>Deploy Workflow</DialogTitle>
|
||||
{needsRedeployment && versions.length > 0 && versionToActivate === null && (
|
||||
<Badge
|
||||
variant='outline'
|
||||
className='border-purple-500/20 bg-purple-500/10 text-purple-600 dark:text-purple-400'
|
||||
>
|
||||
<Badge variant='default'>
|
||||
{versions.find((v) => v.isActive)?.name ||
|
||||
`v${versions.find((v) => v.isActive)?.version}`}{' '}
|
||||
active
|
||||
@@ -858,38 +852,34 @@ export function DeployModal({
|
||||
className='px-4 py-2.5'
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<DropdownMenu
|
||||
<Popover
|
||||
open={openDropdown === v.version}
|
||||
onOpenChange={(open) =>
|
||||
setOpenDropdown(open ? v.version : null)
|
||||
}
|
||||
>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<UIButton
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='icon'
|
||||
className='h-8 w-8'
|
||||
disabled={activatingVersion === v.version}
|
||||
className='h-8 w-8 p-0'
|
||||
>
|
||||
<MoreVertical className='h-4 w-4' />
|
||||
</UIButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align='end'
|
||||
onCloseAutoFocus={(event) => event.preventDefault()}
|
||||
>
|
||||
<DropdownMenuItem
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent align='end'>
|
||||
<PopoverItem
|
||||
onClick={() => openVersionPreview(v.version)}
|
||||
>
|
||||
{v.isActive ? 'View Active' : 'Inspect'}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
</PopoverItem>
|
||||
<PopoverItem
|
||||
onClick={() => handleStartRename(v.version, v.name)}
|
||||
>
|
||||
Rename
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</PopoverItem>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
@@ -905,22 +895,20 @@ export function DeployModal({
|
||||
{versions.length}
|
||||
</span>
|
||||
<div className='flex gap-2'>
|
||||
<UIButton
|
||||
<Button
|
||||
variant='outline'
|
||||
size='sm'
|
||||
onClick={() => setCurrentPage(currentPage - 1)}
|
||||
disabled={currentPage === 1}
|
||||
>
|
||||
Previous
|
||||
</UIButton>
|
||||
<UIButton
|
||||
</Button>
|
||||
<Button
|
||||
variant='outline'
|
||||
size='sm'
|
||||
onClick={() => setCurrentPage(currentPage + 1)}
|
||||
disabled={currentPage * itemsPerPage >= versions.length}
|
||||
>
|
||||
Next
|
||||
</UIButton>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -461,7 +461,26 @@ export function MessagesInput({
|
||||
fieldHandlers.onChange(e)
|
||||
autoResizeTextarea(fieldId)
|
||||
}}
|
||||
onKeyDown={fieldHandlers.onKeyDown}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Tab' && !isPreview && !disabled) {
|
||||
e.preventDefault()
|
||||
const direction = e.shiftKey ? -1 : 1
|
||||
const nextIndex = index + direction
|
||||
|
||||
if (nextIndex >= 0 && nextIndex < currentMessages.length) {
|
||||
const nextFieldId = `message-${nextIndex}`
|
||||
const nextTextarea = textareaRefs.current[nextFieldId]
|
||||
if (nextTextarea) {
|
||||
nextTextarea.focus()
|
||||
nextTextarea.selectionStart = nextTextarea.value.length
|
||||
nextTextarea.selectionEnd = nextTextarea.value.length
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
fieldHandlers.onKeyDown(e)
|
||||
}}
|
||||
onDrop={fieldHandlers.onDrop}
|
||||
onDragOver={fieldHandlers.onDragOver}
|
||||
onScroll={(e) => {
|
||||
|
||||
@@ -373,7 +373,7 @@ export function Variables() {
|
||||
|
||||
return (
|
||||
<div
|
||||
className='fixed z-30 flex flex-col overflow-hidden rounded-[6px] bg-[var(--surface-1)] px-[10px] pt-[2px] pb-[8px]'
|
||||
className='fixed z-30 flex flex-col overflow-hidden rounded-[6px] border border-[var(--border)] bg-[var(--surface-1)] px-[10px] pt-[2px] pb-[8px]'
|
||||
style={{
|
||||
left: `${actualPosition.x}px`,
|
||||
top: `${actualPosition.y}px`,
|
||||
@@ -421,7 +421,7 @@ export function Variables() {
|
||||
{/* Content */}
|
||||
<div className='flex flex-1 flex-col overflow-hidden pt-[8px]'>
|
||||
{workflowVariables.length === 0 ? (
|
||||
<div className='flex flex-1 items-center justify-center text-[13px] text-[var(--text-tertiary)]'>
|
||||
<div className='flex h-full items-center justify-center text-[#8D8D8D] text-[13px]'>
|
||||
{STRINGS.emptyState}
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import * as DialogPrimitive from '@radix-ui/react-dialog'
|
||||
import * as VisuallyHidden from '@radix-ui/react-visually-hidden'
|
||||
import fuzzysort from 'fuzzysort'
|
||||
import { BookOpen, Layout, RepeatIcon, ScrollText, Search, SplitIcon } from 'lucide-react'
|
||||
import { useParams, useRouter } from 'next/navigation'
|
||||
import { Dialog, DialogPortal, DialogTitle } from '@/components/ui/dialog'
|
||||
@@ -98,7 +99,6 @@ export function SearchModal({
|
||||
const workspaceId = params.workspaceId as string
|
||||
const brand = useBrandConfig()
|
||||
|
||||
// Get all available blocks - only when on workflow page
|
||||
const blocks = useMemo(() => {
|
||||
if (!isOnWorkflowPage) return []
|
||||
|
||||
@@ -118,7 +118,6 @@ export function SearchModal({
|
||||
})
|
||||
)
|
||||
|
||||
// Add special blocks (loop and parallel)
|
||||
const specialBlocks: BlockItem[] = [
|
||||
{
|
||||
id: 'loop',
|
||||
@@ -147,7 +146,6 @@ export function SearchModal({
|
||||
const allTriggers = getTriggersForSidebar()
|
||||
const priorityOrder = ['Start', 'Schedule', 'Webhook']
|
||||
|
||||
// Sort triggers with priority order matching toolbar
|
||||
const sortedTriggers = allTriggers.sort((a, b) => {
|
||||
const aIndex = priorityOrder.indexOf(a.name)
|
||||
const bIndex = priorityOrder.indexOf(b.name)
|
||||
@@ -173,7 +171,6 @@ export function SearchModal({
|
||||
)
|
||||
}, [isOnWorkflowPage])
|
||||
|
||||
// Get all available tools - only when on workflow page
|
||||
const tools = useMemo(() => {
|
||||
if (!isOnWorkflowPage) return []
|
||||
|
||||
@@ -192,7 +189,6 @@ export function SearchModal({
|
||||
)
|
||||
}, [isOnWorkflowPage])
|
||||
|
||||
// Define pages
|
||||
const pages = useMemo(
|
||||
(): PageItem[] => [
|
||||
{
|
||||
@@ -218,7 +214,6 @@ export function SearchModal({
|
||||
[workspaceId, brand.documentationUrl]
|
||||
)
|
||||
|
||||
// Define docs
|
||||
const docs = useMemo((): DocItem[] => {
|
||||
const allBlocks = getAllBlocks()
|
||||
const docsItems: DocItem[] = []
|
||||
@@ -238,11 +233,9 @@ export function SearchModal({
|
||||
return docsItems
|
||||
}, [])
|
||||
|
||||
// Combine all items into a single flattened list
|
||||
const allItems = useMemo((): SearchItem[] => {
|
||||
const items: SearchItem[] = []
|
||||
|
||||
// Add workspaces
|
||||
workspaces.forEach((workspace) => {
|
||||
items.push({
|
||||
id: workspace.id,
|
||||
@@ -253,7 +246,6 @@ export function SearchModal({
|
||||
})
|
||||
})
|
||||
|
||||
// Add workflows
|
||||
workflows.forEach((workflow) => {
|
||||
items.push({
|
||||
id: workflow.id,
|
||||
@@ -265,7 +257,6 @@ export function SearchModal({
|
||||
})
|
||||
})
|
||||
|
||||
// Add pages
|
||||
pages.forEach((page) => {
|
||||
items.push({
|
||||
id: page.id,
|
||||
@@ -277,7 +268,6 @@ export function SearchModal({
|
||||
})
|
||||
})
|
||||
|
||||
// Add blocks
|
||||
blocks.forEach((block) => {
|
||||
items.push({
|
||||
id: block.id,
|
||||
@@ -290,7 +280,6 @@ export function SearchModal({
|
||||
})
|
||||
})
|
||||
|
||||
// Add triggers
|
||||
triggers.forEach((trigger) => {
|
||||
items.push({
|
||||
id: trigger.id,
|
||||
@@ -304,7 +293,6 @@ export function SearchModal({
|
||||
})
|
||||
})
|
||||
|
||||
// Add tools
|
||||
tools.forEach((tool) => {
|
||||
items.push({
|
||||
id: tool.id,
|
||||
@@ -317,7 +305,6 @@ export function SearchModal({
|
||||
})
|
||||
})
|
||||
|
||||
// Add docs
|
||||
docs.forEach((doc) => {
|
||||
items.push({
|
||||
id: doc.id,
|
||||
@@ -336,7 +323,6 @@ export function SearchModal({
|
||||
[]
|
||||
)
|
||||
|
||||
// Filter items based on search query and enforce section ordering
|
||||
const filteredItems = useMemo(() => {
|
||||
const orderMap = sectionOrder.reduce<Record<SearchItem['type'], number>>(
|
||||
(acc, type, index) => {
|
||||
@@ -346,27 +332,48 @@ export function SearchModal({
|
||||
{} as Record<SearchItem['type'], number>
|
||||
)
|
||||
|
||||
const baseItems = !searchQuery.trim()
|
||||
? allItems
|
||||
: allItems.filter(
|
||||
(item) =>
|
||||
item.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
item.description?.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
)
|
||||
|
||||
return [...baseItems].sort((a, b) => {
|
||||
if (!searchQuery.trim()) {
|
||||
return [...allItems].sort((a, b) => {
|
||||
const aOrder = orderMap[a.type] ?? Number.MAX_SAFE_INTEGER
|
||||
const bOrder = orderMap[b.type] ?? Number.MAX_SAFE_INTEGER
|
||||
return aOrder - bOrder
|
||||
})
|
||||
}
|
||||
|
||||
const results = fuzzysort.go(searchQuery, allItems, {
|
||||
keys: ['name', 'description'],
|
||||
limit: 100,
|
||||
threshold: -1000,
|
||||
all: true,
|
||||
scoreFn: (a) => {
|
||||
const nameScore = a[0] ? a[0].score : Number.NEGATIVE_INFINITY
|
||||
const descScore = a[1] ? a[1].score : Number.NEGATIVE_INFINITY
|
||||
|
||||
return Math.max(nameScore * 2, descScore)
|
||||
},
|
||||
})
|
||||
|
||||
return results
|
||||
.map((result) => ({
|
||||
item: result.obj,
|
||||
score: result.score,
|
||||
}))
|
||||
.sort((a, b) => {
|
||||
if (Math.abs(a.score - b.score) > 100) {
|
||||
return b.score - a.score
|
||||
}
|
||||
|
||||
const aOrder = orderMap[a.item.type] ?? Number.MAX_SAFE_INTEGER
|
||||
const bOrder = orderMap[b.item.type] ?? Number.MAX_SAFE_INTEGER
|
||||
return aOrder - bOrder
|
||||
})
|
||||
.map((result) => result.item)
|
||||
}, [allItems, searchQuery, sectionOrder])
|
||||
|
||||
// Reset selected index when filtered items change
|
||||
useEffect(() => {
|
||||
setSelectedIndex(0)
|
||||
}, [filteredItems])
|
||||
|
||||
// Clear search when modal closes
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
setSearchQuery('')
|
||||
@@ -374,7 +381,6 @@ export function SearchModal({
|
||||
}
|
||||
}, [open])
|
||||
|
||||
// Handle item selection
|
||||
const handleItemClick = useCallback(
|
||||
(item: SearchItem) => {
|
||||
switch (item.type) {
|
||||
@@ -411,7 +417,6 @@ export function SearchModal({
|
||||
[router, onOpenChange]
|
||||
)
|
||||
|
||||
// Handle keyboard navigation
|
||||
useEffect(() => {
|
||||
if (!open) return
|
||||
|
||||
@@ -442,7 +447,6 @@ export function SearchModal({
|
||||
return () => document.removeEventListener('keydown', handleKeyDown)
|
||||
}, [open, selectedIndex, filteredItems, handleItemClick, onOpenChange])
|
||||
|
||||
// Scroll selected item into view
|
||||
useEffect(() => {
|
||||
if (open && selectedIndex >= 0) {
|
||||
const element = document.querySelector(`[data-search-item-index="${selectedIndex}"]`)
|
||||
@@ -455,7 +459,6 @@ export function SearchModal({
|
||||
}
|
||||
}, [selectedIndex, open])
|
||||
|
||||
// Group items by type for sectioned display
|
||||
const groupedItems = useMemo(() => {
|
||||
const groups: Record<string, SearchItem[]> = {
|
||||
workspace: [],
|
||||
@@ -476,7 +479,6 @@ export function SearchModal({
|
||||
return groups
|
||||
}, [filteredItems])
|
||||
|
||||
// Section titles mapping
|
||||
const sectionTitles: Record<string, string> = {
|
||||
workspace: 'Workspaces',
|
||||
workflow: 'Workflows',
|
||||
|
||||
300
apps/sim/blocks/blocks/calendly.ts
Normal file
300
apps/sim/blocks/blocks/calendly.ts
Normal file
@@ -0,0 +1,300 @@
|
||||
import { CalendlyIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import type { ToolResponse } from '@/tools/types'
|
||||
import { getTrigger } from '@/triggers'
|
||||
|
||||
export const CalendlyBlock: BlockConfig<ToolResponse> = {
|
||||
type: 'calendly',
|
||||
name: 'Calendly',
|
||||
description: 'Manage Calendly scheduling and events',
|
||||
authMode: AuthMode.ApiKey,
|
||||
triggerAllowed: true,
|
||||
longDescription:
|
||||
'Integrate Calendly into your workflow. Manage event types, scheduled events, invitees, and webhooks. Can also trigger workflows based on Calendly webhook events (invitee scheduled, invitee canceled, routing form submitted). Requires Personal Access Token.',
|
||||
docsLink: 'https://docs.sim.ai/tools/calendly',
|
||||
category: 'tools',
|
||||
bgColor: '#FFFFFF',
|
||||
icon: CalendlyIcon,
|
||||
subBlocks: [
|
||||
{
|
||||
id: 'operation',
|
||||
title: 'Operation',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'Get Current User', id: 'calendly_get_current_user' },
|
||||
{ label: 'List Event Types', id: 'calendly_list_event_types' },
|
||||
{ label: 'Get Event Type', id: 'calendly_get_event_type' },
|
||||
{ label: 'List Scheduled Events', id: 'calendly_list_scheduled_events' },
|
||||
{ label: 'Get Scheduled Event', id: 'calendly_get_scheduled_event' },
|
||||
{ label: 'List Event Invitees', id: 'calendly_list_event_invitees' },
|
||||
{ label: 'Cancel Event', id: 'calendly_cancel_event' },
|
||||
],
|
||||
value: () => 'calendly_list_scheduled_events',
|
||||
},
|
||||
{
|
||||
id: 'apiKey',
|
||||
title: 'Personal Access Token',
|
||||
type: 'short-input',
|
||||
placeholder: 'Enter your Calendly personal access token',
|
||||
password: true,
|
||||
required: true,
|
||||
},
|
||||
// Get Event Type fields
|
||||
{
|
||||
id: 'eventTypeUuid',
|
||||
title: 'Event Type UUID',
|
||||
type: 'short-input',
|
||||
placeholder: 'Enter event type UUID or URI',
|
||||
required: true,
|
||||
condition: { field: 'operation', value: 'calendly_get_event_type' },
|
||||
},
|
||||
// List Event Types fields
|
||||
{
|
||||
id: 'user',
|
||||
title: 'User URI',
|
||||
type: 'short-input',
|
||||
placeholder: 'Filter by user URI',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['calendly_list_event_types', 'calendly_list_scheduled_events'],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'organization',
|
||||
title: 'Organization URI',
|
||||
type: 'short-input',
|
||||
placeholder: 'Filter by organization URI (optional)',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['calendly_list_event_types', 'calendly_list_scheduled_events'],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'active',
|
||||
title: 'Active Only',
|
||||
type: 'switch',
|
||||
description:
|
||||
'When enabled, shows only active event types. When disabled, shows all event types.',
|
||||
condition: { field: 'operation', value: 'calendly_list_event_types' },
|
||||
},
|
||||
// List Scheduled Events fields
|
||||
{
|
||||
id: 'invitee_email',
|
||||
title: 'Invitee Email',
|
||||
type: 'short-input',
|
||||
placeholder: 'Filter by invitee email',
|
||||
condition: { field: 'operation', value: 'calendly_list_scheduled_events' },
|
||||
},
|
||||
{
|
||||
id: 'min_start_time',
|
||||
title: 'Min Start Time',
|
||||
type: 'short-input',
|
||||
placeholder: 'ISO 8601 format (e.g., 2024-01-01T00:00:00Z)',
|
||||
condition: { field: 'operation', value: 'calendly_list_scheduled_events' },
|
||||
},
|
||||
{
|
||||
id: 'max_start_time',
|
||||
title: 'Max Start Time',
|
||||
type: 'short-input',
|
||||
placeholder: 'ISO 8601 format (e.g., 2024-12-31T23:59:59Z)',
|
||||
condition: { field: 'operation', value: 'calendly_list_scheduled_events' },
|
||||
},
|
||||
{
|
||||
id: 'status',
|
||||
title: 'Status',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'All', id: '' },
|
||||
{ label: 'Active', id: 'active' },
|
||||
{ label: 'Canceled', id: 'canceled' },
|
||||
],
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['calendly_list_scheduled_events', 'calendly_list_event_invitees'],
|
||||
},
|
||||
},
|
||||
// Get Scheduled Event / List Invitees / Cancel Event fields
|
||||
{
|
||||
id: 'eventUuid',
|
||||
title: 'Event UUID',
|
||||
type: 'short-input',
|
||||
placeholder: 'Enter scheduled event UUID or URI',
|
||||
required: true,
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: [
|
||||
'calendly_get_scheduled_event',
|
||||
'calendly_list_event_invitees',
|
||||
'calendly_cancel_event',
|
||||
],
|
||||
},
|
||||
},
|
||||
// Cancel Event fields
|
||||
{
|
||||
id: 'reason',
|
||||
title: 'Cancellation Reason',
|
||||
type: 'long-input',
|
||||
placeholder: 'Reason for cancellation (optional)',
|
||||
condition: { field: 'operation', value: 'calendly_cancel_event' },
|
||||
},
|
||||
// List Event Invitees fields
|
||||
{
|
||||
id: 'email',
|
||||
title: 'Email',
|
||||
type: 'short-input',
|
||||
placeholder: 'Filter by invitee email',
|
||||
condition: { field: 'operation', value: 'calendly_list_event_invitees' },
|
||||
},
|
||||
// Pagination fields
|
||||
{
|
||||
id: 'count',
|
||||
title: 'Results Per Page',
|
||||
type: 'short-input',
|
||||
placeholder: 'Number of results (default: 20, max: 100)',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: [
|
||||
'calendly_list_event_types',
|
||||
'calendly_list_scheduled_events',
|
||||
'calendly_list_event_invitees',
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'pageToken',
|
||||
title: 'Page Token',
|
||||
type: 'short-input',
|
||||
placeholder: 'Token for pagination',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: [
|
||||
'calendly_list_event_types',
|
||||
'calendly_list_scheduled_events',
|
||||
'calendly_list_event_invitees',
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'sort',
|
||||
title: 'Sort Order',
|
||||
type: 'short-input',
|
||||
placeholder: 'e.g., "name:asc", "start_time:desc"',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: [
|
||||
'calendly_list_event_types',
|
||||
'calendly_list_scheduled_events',
|
||||
'calendly_list_event_invitees',
|
||||
],
|
||||
},
|
||||
},
|
||||
// Trigger SubBlocks
|
||||
...getTrigger('calendly_invitee_created').subBlocks,
|
||||
...getTrigger('calendly_invitee_canceled').subBlocks,
|
||||
...getTrigger('calendly_routing_form_submitted').subBlocks,
|
||||
...getTrigger('calendly_webhook').subBlocks,
|
||||
],
|
||||
tools: {
|
||||
access: [
|
||||
'calendly_get_current_user',
|
||||
'calendly_list_event_types',
|
||||
'calendly_get_event_type',
|
||||
'calendly_list_scheduled_events',
|
||||
'calendly_get_scheduled_event',
|
||||
'calendly_list_event_invitees',
|
||||
'calendly_cancel_event',
|
||||
],
|
||||
config: {
|
||||
tool: (params) => {
|
||||
return params.operation || 'calendly_list_scheduled_events'
|
||||
},
|
||||
params: (params) => {
|
||||
const { operation, events, ...rest } = params
|
||||
|
||||
let parsedEvents: any | undefined
|
||||
|
||||
try {
|
||||
if (events) parsedEvents = JSON.parse(events)
|
||||
} catch (error: any) {
|
||||
throw new Error(`Invalid JSON input for events: ${error.message}`)
|
||||
}
|
||||
|
||||
return {
|
||||
...rest,
|
||||
...(parsedEvents && { events: parsedEvents }),
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
inputs: {
|
||||
operation: { type: 'string', description: 'Operation to perform' },
|
||||
apiKey: { type: 'string', description: 'Personal access token' },
|
||||
// Event Type params
|
||||
eventTypeUuid: { type: 'string', description: 'Event type UUID' },
|
||||
user: { type: 'string', description: 'User URI filter' },
|
||||
organization: { type: 'string', description: 'Organization URI' },
|
||||
active: { type: 'boolean', description: 'Filter by active status' },
|
||||
// Scheduled Event params
|
||||
eventUuid: { type: 'string', description: 'Scheduled event UUID' },
|
||||
invitee_email: { type: 'string', description: 'Filter by invitee email' },
|
||||
min_start_time: { type: 'string', description: 'Minimum start time (ISO 8601)' },
|
||||
max_start_time: { type: 'string', description: 'Maximum start time (ISO 8601)' },
|
||||
status: { type: 'string', description: 'Status filter (active or canceled)' },
|
||||
// Cancel Event params
|
||||
reason: { type: 'string', description: 'Cancellation reason' },
|
||||
// Invitees params
|
||||
email: { type: 'string', description: 'Filter by email' },
|
||||
// Pagination params
|
||||
count: { type: 'number', description: 'Results per page' },
|
||||
pageToken: { type: 'string', description: 'Pagination token' },
|
||||
sort: { type: 'string', description: 'Sort order' },
|
||||
// Webhook params
|
||||
webhookUuid: { type: 'string', description: 'Webhook UUID' },
|
||||
url: { type: 'string', description: 'Webhook callback URL' },
|
||||
events: { type: 'json', description: 'Array of event types' },
|
||||
scope: { type: 'string', description: 'Webhook scope' },
|
||||
signing_key: { type: 'string', description: 'Webhook signing key' },
|
||||
},
|
||||
outputs: {
|
||||
// Common outputs
|
||||
success: { type: 'boolean', description: 'Whether operation succeeded' },
|
||||
// User outputs
|
||||
resource: { type: 'json', description: 'Resource data (user, event type, event, etc.)' },
|
||||
// List outputs
|
||||
collection: { type: 'json', description: 'Array of items' },
|
||||
pagination: { type: 'json', description: 'Pagination information' },
|
||||
// Event details
|
||||
uri: { type: 'string', description: 'Resource URI' },
|
||||
name: { type: 'string', description: 'Resource name' },
|
||||
email: { type: 'string', description: 'Email address' },
|
||||
status: { type: 'string', description: 'Status' },
|
||||
start_time: { type: 'string', description: 'Event start time (ISO 8601)' },
|
||||
end_time: { type: 'string', description: 'Event end time (ISO 8601)' },
|
||||
location: { type: 'json', description: 'Event location details' },
|
||||
scheduling_url: { type: 'string', description: 'Scheduling page URL' },
|
||||
// Webhook outputs
|
||||
callback_url: { type: 'string', description: 'Webhook URL' },
|
||||
signing_key: { type: 'string', description: 'Webhook signing key' },
|
||||
// Delete outputs
|
||||
deleted: { type: 'boolean', description: 'Whether deletion succeeded' },
|
||||
message: { type: 'string', description: 'Status message' },
|
||||
// Trigger outputs
|
||||
event: { type: 'string', description: 'Webhook event type' },
|
||||
created_at: { type: 'string', description: 'Webhook event creation timestamp' },
|
||||
created_by: {
|
||||
type: 'string',
|
||||
description: 'URI of the Calendly user who created this webhook',
|
||||
},
|
||||
payload: { type: 'json', description: 'Complete webhook payload data' },
|
||||
},
|
||||
triggers: {
|
||||
enabled: true,
|
||||
available: [
|
||||
'calendly_invitee_created',
|
||||
'calendly_invitee_canceled',
|
||||
'calendly_routing_form_submitted',
|
||||
'calendly_webhook',
|
||||
],
|
||||
},
|
||||
}
|
||||
@@ -13,7 +13,7 @@ export const McpBlock: BlockConfig<McpResponse> = {
|
||||
description: 'Execute tools from Model Context Protocol (MCP) servers',
|
||||
longDescription:
|
||||
'Integrate MCP into the workflow. Can execute tools from MCP servers. Requires MCP servers in workspace settings.',
|
||||
docsLink: 'https://docs.sim.ai/tools/mcp',
|
||||
docsLink: 'https://docs.sim.ai/mcp',
|
||||
category: 'tools',
|
||||
bgColor: '#181C1E',
|
||||
icon: ServerIcon,
|
||||
|
||||
@@ -31,7 +31,7 @@ export const MemoryBlock: BlockConfig = {
|
||||
value: () => 'add',
|
||||
},
|
||||
{
|
||||
id: 'conversationId',
|
||||
id: 'id',
|
||||
title: 'Conversation ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'Enter conversation ID (e.g., user-123)',
|
||||
@@ -53,7 +53,7 @@ export const MemoryBlock: BlockConfig = {
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
id: 'conversationId',
|
||||
id: 'id',
|
||||
title: 'Conversation ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'Enter conversation ID (e.g., user-123)',
|
||||
@@ -86,7 +86,7 @@ export const MemoryBlock: BlockConfig = {
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
id: 'conversationId',
|
||||
id: 'id',
|
||||
title: 'Conversation ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'Enter conversation ID (e.g., user-123)',
|
||||
@@ -171,8 +171,10 @@ export const MemoryBlock: BlockConfig = {
|
||||
errors.push('Operation is required')
|
||||
}
|
||||
|
||||
const conversationId = params.id || params.conversationId
|
||||
|
||||
if (params.operation === 'add') {
|
||||
if (!params.conversationId) {
|
||||
if (!conversationId) {
|
||||
errors.push('Conversation ID is required for add operation')
|
||||
}
|
||||
if (!params.role) {
|
||||
@@ -184,9 +186,9 @@ export const MemoryBlock: BlockConfig = {
|
||||
}
|
||||
|
||||
if (params.operation === 'get' || params.operation === 'delete') {
|
||||
if (!params.conversationId && !params.blockId && !params.blockName) {
|
||||
if (!conversationId && !params.blockId && !params.blockName) {
|
||||
errors.push(
|
||||
`At least one of conversationId, blockId, or blockName is required for ${params.operation} operation`
|
||||
`At least one of ID, blockId, or blockName is required for ${params.operation} operation`
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -200,7 +202,7 @@ export const MemoryBlock: BlockConfig = {
|
||||
if (params.operation === 'add') {
|
||||
const result: Record<string, any> = {
|
||||
...baseResult,
|
||||
conversationId: params.conversationId,
|
||||
conversationId: conversationId,
|
||||
role: params.role,
|
||||
content: params.content,
|
||||
}
|
||||
@@ -213,7 +215,7 @@ export const MemoryBlock: BlockConfig = {
|
||||
|
||||
if (params.operation === 'get') {
|
||||
const result: Record<string, any> = { ...baseResult }
|
||||
if (params.conversationId) result.conversationId = params.conversationId
|
||||
if (conversationId) result.conversationId = conversationId
|
||||
if (params.blockId) result.blockId = params.blockId
|
||||
if (params.blockName) result.blockName = params.blockName
|
||||
return result
|
||||
@@ -221,7 +223,7 @@ export const MemoryBlock: BlockConfig = {
|
||||
|
||||
if (params.operation === 'delete') {
|
||||
const result: Record<string, any> = { ...baseResult }
|
||||
if (params.conversationId) result.conversationId = params.conversationId
|
||||
if (conversationId) result.conversationId = conversationId
|
||||
if (params.blockId) result.blockId = params.blockId
|
||||
if (params.blockName) result.blockName = params.blockName
|
||||
return result
|
||||
|
||||
696
apps/sim/blocks/blocks/neo4j.ts
Normal file
696
apps/sim/blocks/blocks/neo4j.ts
Normal file
@@ -0,0 +1,696 @@
|
||||
import { Neo4jIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import type { Neo4jResponse } from '@/tools/neo4j/types'
|
||||
|
||||
export const Neo4jBlock: BlockConfig<Neo4jResponse> = {
|
||||
type: 'neo4j',
|
||||
name: 'Neo4j',
|
||||
description: 'Connect to Neo4j graph database',
|
||||
longDescription:
|
||||
'Integrate Neo4j graph database into the workflow. Can query, create, merge, update, and delete nodes and relationships.',
|
||||
docsLink: 'https://docs.sim.ai/tools/neo4j',
|
||||
category: 'tools',
|
||||
bgColor: '#FFFFFF',
|
||||
icon: Neo4jIcon,
|
||||
subBlocks: [
|
||||
{
|
||||
id: 'operation',
|
||||
title: 'Operation',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'Query (MATCH)', id: 'query' },
|
||||
{ label: 'Create Nodes/Relationships', id: 'create' },
|
||||
{ label: 'Merge (Find or Create)', id: 'merge' },
|
||||
{ label: 'Update Properties (SET)', id: 'update' },
|
||||
{ label: 'Delete Nodes/Relationships', id: 'delete' },
|
||||
{ label: 'Execute Cypher', id: 'execute' },
|
||||
],
|
||||
value: () => 'query',
|
||||
},
|
||||
{
|
||||
id: 'host',
|
||||
title: 'Host',
|
||||
type: 'short-input',
|
||||
placeholder: 'localhost or your.neo4j.host',
|
||||
required: true,
|
||||
password: true,
|
||||
},
|
||||
{
|
||||
id: 'port',
|
||||
title: 'Port',
|
||||
type: 'short-input',
|
||||
placeholder: '7687',
|
||||
value: () => '7687',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'database',
|
||||
title: 'Database Name',
|
||||
type: 'short-input',
|
||||
placeholder: 'neo4j',
|
||||
value: () => 'neo4j',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'username',
|
||||
title: 'Username',
|
||||
type: 'short-input',
|
||||
placeholder: 'neo4j',
|
||||
value: () => 'neo4j',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'password',
|
||||
title: 'Password',
|
||||
type: 'short-input',
|
||||
password: true,
|
||||
placeholder: 'Your database password',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'encryption',
|
||||
title: 'Encryption',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'Disabled', id: 'disabled' },
|
||||
{ label: 'Enabled (TLS/SSL)', id: 'enabled' },
|
||||
],
|
||||
value: () => 'disabled',
|
||||
},
|
||||
{
|
||||
id: 'cypherQuery',
|
||||
title: 'Cypher Query',
|
||||
type: 'code',
|
||||
placeholder: 'MATCH (n:Person) WHERE n.age > 21 RETURN n LIMIT 10',
|
||||
required: true,
|
||||
condition: { field: 'operation', value: 'query' },
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
maintainHistory: true,
|
||||
prompt: `You are an expert Neo4j and Cypher developer. Generate Cypher queries based on the user's request.
|
||||
|
||||
### CONTEXT
|
||||
{context}
|
||||
|
||||
### CRITICAL INSTRUCTION
|
||||
Return ONLY the Cypher query. Do not include any explanations, markdown formatting, comments, or additional text. Just the raw Cypher query.
|
||||
|
||||
### QUERY GUIDELINES
|
||||
1. **Pattern Matching**: Use MATCH to find patterns in the graph
|
||||
2. **Filtering**: Use WHERE clauses for conditions
|
||||
3. **Return**: Specify what to return with RETURN
|
||||
4. **Performance**: Use indexes when possible
|
||||
5. **Limit Results**: ALWAYS include LIMIT to prevent large result sets
|
||||
|
||||
### IMPORTANT: LIMIT Best Practices
|
||||
- Always include LIMIT in your queries to prevent performance issues
|
||||
- Use parameterized LIMIT for flexibility: LIMIT $limit
|
||||
- Place LIMIT at the end after ORDER BY and SKIP clauses
|
||||
- Example: MATCH (n:Person) RETURN n ORDER BY n.name LIMIT $limit
|
||||
|
||||
### CYPHER QUERY PATTERNS
|
||||
|
||||
**Basic Node Match with LIMIT**:
|
||||
MATCH (n:Person) RETURN n LIMIT 25
|
||||
|
||||
**Match with Parameterized LIMIT (Recommended)**:
|
||||
MATCH (n:Person) RETURN n LIMIT $limit
|
||||
|
||||
**Match with Properties**:
|
||||
MATCH (n:Person {name: "Alice"}) RETURN n
|
||||
|
||||
**Match with Parameters (Recommended)**:
|
||||
MATCH (n:Person {name: $name}) RETURN n LIMIT 100
|
||||
|
||||
**Match with WHERE and Parameters**:
|
||||
MATCH (n:Person) WHERE n.age > $minAge RETURN n.name, n.age LIMIT $limit
|
||||
|
||||
**Match Relationship**:
|
||||
MATCH (p:Person)-[:KNOWS]->(friend:Person) RETURN p.name, friend.name
|
||||
|
||||
**Match with Relationship Properties**:
|
||||
MATCH (p:Person)-[r:RATED {rating: 5}]->(m:Movie) RETURN p.name, m.title
|
||||
|
||||
**Pattern with Multiple Nodes**:
|
||||
MATCH (p:Person)-[:ACTED_IN]->(m:Movie)<-[:DIRECTED]-(d:Person) RETURN p.name, m.title, d.name
|
||||
|
||||
**Variable Length Paths**:
|
||||
MATCH (p1:Person)-[:KNOWS*1..3]-(p2:Person) WHERE p1.name = $name RETURN p2.name
|
||||
|
||||
**Shortest Path**:
|
||||
MATCH path = shortestPath((p1:Person)-[:KNOWS*]-(p2:Person)) WHERE p1.name = $name1 AND p2.name = $name2 RETURN path
|
||||
|
||||
**Complex WHERE with Parameters**:
|
||||
MATCH (p:Person) WHERE p.age > $minAge AND p.age < $maxAge AND p.country = $country RETURN p
|
||||
|
||||
### EXAMPLES
|
||||
|
||||
**Find all nodes**: MATCH (n:Person) RETURN n LIMIT 10
|
||||
**Find by property**: MATCH (n:Person) WHERE n.name = "Alice" RETURN n
|
||||
**Find with parameters**: MATCH (n:Person) WHERE n.name = $name AND n.age > $minAge RETURN n
|
||||
**Find relationships**: MATCH (p:Person)-[r:KNOWS]->(f:Person) RETURN p.name, type(r), f.name
|
||||
**Find with multiple labels**: MATCH (n:Person:Employee) RETURN n
|
||||
**Aggregate data**: MATCH (p:Person) RETURN p.country, count(p) as personCount ORDER BY personCount DESC
|
||||
**Range query with params**: MATCH (p:Product) WHERE p.price >= $minPrice AND p.price <= $maxPrice RETURN p
|
||||
|
||||
### NOTE
|
||||
Use the Parameters field for dynamic values to improve security and query performance.
|
||||
|
||||
Return ONLY the Cypher query - no explanations.`,
|
||||
placeholder: 'Describe what you want to query...',
|
||||
generationType: 'neo4j-cypher',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'cypherQuery',
|
||||
title: 'Cypher CREATE Statement',
|
||||
type: 'code',
|
||||
placeholder: 'CREATE (n:Person {name: "Alice", age: 30})',
|
||||
required: true,
|
||||
condition: { field: 'operation', value: 'create' },
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
maintainHistory: true,
|
||||
prompt: `You are an expert Neo4j developer. Generate Cypher CREATE statements to add new nodes and relationships.
|
||||
|
||||
### CONTEXT
|
||||
{context}
|
||||
|
||||
### CRITICAL INSTRUCTION
|
||||
Return ONLY the Cypher CREATE statement. No explanations, no markdown, just the raw Cypher query.
|
||||
|
||||
### ⚠️ DUPLICATE WARNING
|
||||
CREATE always creates new nodes/relationships, even if identical ones exist.
|
||||
- Use MERGE operation if you want to avoid duplicates (find-or-create behavior)
|
||||
- Use CREATE only when you want to guarantee new entities
|
||||
|
||||
### CREATE PATTERNS
|
||||
|
||||
**Create Single Node**:
|
||||
CREATE (n:Person {name: "Alice", age: 30, email: "alice@example.com"})
|
||||
|
||||
**Create with Parameters (Recommended)**:
|
||||
CREATE (n:Person {name: $name, age: $age, email: $email}) RETURN n
|
||||
|
||||
**Create Multiple Nodes**:
|
||||
CREATE (n1:Person {name: "Alice"}), (n2:Person {name: "Bob"})
|
||||
|
||||
**Create Node with Relationship**:
|
||||
CREATE (p:Person {name: "Alice"})-[:KNOWS {since: 2020}]->(f:Person {name: "Bob"})
|
||||
|
||||
**Create Relationship Between Existing Nodes**:
|
||||
MATCH (a:Person {name: "Alice"}), (b:Person {name: "Bob"})
|
||||
CREATE (a)-[:KNOWS {since: 2024}]->(b)
|
||||
|
||||
**Create with Parameters and Return**:
|
||||
CREATE (n:Person {name: $name, age: $age}) RETURN n
|
||||
|
||||
**Batch Create with Parameters**:
|
||||
UNWIND $users AS user
|
||||
CREATE (n:Person {name: user.name, email: user.email})
|
||||
|
||||
### EXAMPLES
|
||||
Create person: CREATE (n:Person {name: "Alice", age: 30})
|
||||
Create with params: CREATE (n:Person {name: $name, age: $age}) RETURN n
|
||||
Create with relationship: CREATE (p:Person {name: "Alice"})-[:WORKS_AT]->(c:Company {name: "Acme"})
|
||||
Create multiple: CREATE (a:Person {name: "Alice"}), (b:Person {name: "Bob"}), (a)-[:KNOWS]->(b)
|
||||
Batch create: UNWIND $items AS item CREATE (n:Product {name: item.name, price: item.price})
|
||||
|
||||
### NOTE
|
||||
Use the Parameters field with CREATE for dynamic values and security.
|
||||
|
||||
Return ONLY the Cypher CREATE statement.`,
|
||||
placeholder: 'Describe what you want to create...',
|
||||
generationType: 'neo4j-cypher',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'cypherQuery',
|
||||
title: 'Cypher MERGE Statement',
|
||||
type: 'code',
|
||||
placeholder:
|
||||
'MERGE (n:Person {email: "alice@example.com"}) ON CREATE SET n.created = timestamp() RETURN n',
|
||||
required: true,
|
||||
condition: { field: 'operation', value: 'merge' },
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
maintainHistory: true,
|
||||
prompt: `You are an expert Neo4j developer. Generate Cypher MERGE statements for find-or-create operations.
|
||||
|
||||
### CONTEXT
|
||||
{context}
|
||||
|
||||
### CRITICAL INSTRUCTION
|
||||
Return ONLY the Cypher MERGE statement. No explanations, no markdown, just the raw Cypher query.
|
||||
|
||||
### MERGE PATTERNS
|
||||
|
||||
**Basic Merge**:
|
||||
MERGE (n:Person {email: "alice@example.com"})
|
||||
|
||||
**Merge with ON CREATE**:
|
||||
MERGE (n:Person {email: "alice@example.com"})
|
||||
ON CREATE SET n.created = timestamp(), n.name = "Alice"
|
||||
|
||||
**Merge with ON MATCH**:
|
||||
MERGE (n:Person {email: "alice@example.com"})
|
||||
ON MATCH SET n.lastSeen = timestamp()
|
||||
|
||||
**Merge with Both**:
|
||||
MERGE (n:Person {email: "alice@example.com"})
|
||||
ON CREATE SET n.created = timestamp(), n.name = "Alice"
|
||||
ON MATCH SET n.lastSeen = timestamp()
|
||||
|
||||
**Merge Relationship**:
|
||||
MATCH (p:Person {name: "Alice"}), (c:Company {name: "Acme"})
|
||||
MERGE (p)-[:WORKS_AT {since: 2024}]->(c)
|
||||
|
||||
### EXAMPLES
|
||||
Merge person: MERGE (n:Person {email: "alice@example.com"}) ON CREATE SET n.created = timestamp()
|
||||
Merge relationship: MERGE (p:Person {name: "Alice"})-[:KNOWS]->(f:Person {name: "Bob"})
|
||||
|
||||
Return ONLY the Cypher MERGE statement.`,
|
||||
placeholder: 'Describe what you want to merge...',
|
||||
generationType: 'neo4j-cypher',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'cypherQuery',
|
||||
title: 'Cypher UPDATE Statement',
|
||||
type: 'code',
|
||||
placeholder: 'MATCH (n:Person {name: "Alice"}) SET n.age = 31, n.updated = timestamp()',
|
||||
required: true,
|
||||
condition: { field: 'operation', value: 'update' },
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
maintainHistory: true,
|
||||
prompt: `You are an expert Neo4j developer. Generate Cypher UPDATE statements using MATCH and SET.
|
||||
|
||||
### CONTEXT
|
||||
{context}
|
||||
|
||||
### CRITICAL INSTRUCTION
|
||||
Return ONLY the Cypher statement with MATCH and SET. No explanations, no markdown, just the raw Cypher query.
|
||||
|
||||
### UPDATE PATTERNS
|
||||
|
||||
**Update Single Property**:
|
||||
MATCH (n:Person {name: "Alice"}) SET n.age = 31
|
||||
|
||||
**Update Multiple Properties**:
|
||||
MATCH (n:Person {name: "Alice"}) SET n.age = 31, n.city = "NYC", n.updated = timestamp()
|
||||
|
||||
**Update with WHERE**:
|
||||
MATCH (n:Person) WHERE n.age > 30 SET n.category = "senior"
|
||||
|
||||
**Update with Parameters**:
|
||||
MATCH (n:Person {name: $name}) SET n.age = $age, n.city = $city
|
||||
|
||||
**Merge Properties (Safe)**:
|
||||
MATCH (n:Person {name: "Alice"}) SET n += {age: 31, email: "alice@example.com"}
|
||||
|
||||
**⚠️ DANGEROUS - Replace All Properties (removes unlisted properties)**:
|
||||
MATCH (n:Person {name: "Alice"}) SET n = {name: "Alice", age: 31}
|
||||
Note: This removes ALL properties not specified. Use SET n += {...} instead to merge properties safely.
|
||||
|
||||
**Add Property**:
|
||||
MATCH (n:Person {name: "Alice"}) SET n.verified = true
|
||||
|
||||
**Remove Property**:
|
||||
MATCH (n:Person {name: "Alice"}) REMOVE n.temporaryField
|
||||
|
||||
**Add Label**:
|
||||
MATCH (n:Person {name: "Alice"}) SET n:Employee
|
||||
|
||||
**Increment Counter**:
|
||||
MATCH (n:Person {name: "Alice"}) SET n.loginCount = n.loginCount + 1
|
||||
|
||||
### EXAMPLES
|
||||
Update age: MATCH (n:Person {name: "Alice"}) SET n.age = 31
|
||||
Update multiple: MATCH (n:Person) WHERE n.city = "NYC" SET n.verified = true, n.updated = timestamp()
|
||||
With parameters: MATCH (n:Person) WHERE n.email = $email SET n.lastLogin = timestamp()
|
||||
Merge properties safely: MATCH (n:Person {id: $userId}) SET n += {status: "active", verified: true}
|
||||
|
||||
### SAFETY NOTE
|
||||
Use SET n += {...} to merge properties safely. Avoid SET n = {...} unless you explicitly want to replace ALL properties.
|
||||
|
||||
Return ONLY the Cypher update statement.`,
|
||||
placeholder: 'Describe what you want to update...',
|
||||
generationType: 'neo4j-cypher',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'cypherQuery',
|
||||
title: 'Cypher DELETE Statement',
|
||||
type: 'code',
|
||||
placeholder: 'MATCH (n:Person {name: "Alice"}) DETACH DELETE n',
|
||||
required: true,
|
||||
condition: { field: 'operation', value: 'delete' },
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
maintainHistory: true,
|
||||
prompt: `You are an expert Neo4j developer. Generate Cypher DELETE statements to remove nodes and relationships.
|
||||
|
||||
### CONTEXT
|
||||
{context}
|
||||
|
||||
### CRITICAL INSTRUCTION
|
||||
Return ONLY the Cypher DELETE statement. No explanations, no markdown, just the raw Cypher query.
|
||||
|
||||
### ⚠️ DELETION WARNING ⚠️
|
||||
DELETIONS ARE PERMANENT! Be extremely careful and specific with your criteria.
|
||||
|
||||
### DELETE PATTERNS
|
||||
|
||||
**Delete Node (must have no relationships)**:
|
||||
MATCH (n:Person {name: "Alice"}) DELETE n
|
||||
|
||||
**DETACH DELETE (removes relationships first)**:
|
||||
MATCH (n:Person {name: "Alice"}) DETACH DELETE n
|
||||
|
||||
**Delete Relationship Only**:
|
||||
MATCH (p:Person {name: "Alice"})-[r:KNOWS]->(f:Person) DELETE r
|
||||
|
||||
**Delete with WHERE**:
|
||||
MATCH (n:Person) WHERE n.status = "inactive" DETACH DELETE n
|
||||
|
||||
**Delete Multiple Nodes**:
|
||||
MATCH (n:TempNode) WHERE n.created < timestamp() - 86400000 DETACH DELETE n
|
||||
|
||||
### SAFETY
|
||||
- Always use DETACH DELETE for nodes with relationships
|
||||
- Use specific WHERE clauses to target exact nodes
|
||||
- Test with MATCH first to see what will be deleted
|
||||
- Prefer unique identifiers when deleting
|
||||
|
||||
### EXAMPLES
|
||||
Delete person: MATCH (n:Person {email: "alice@example.com"}) DETACH DELETE n
|
||||
Delete relationship: MATCH (p:Person)-[r:KNOWS]->(f:Person) WHERE p.name = "Alice" DELETE r
|
||||
Delete old data: MATCH (n:TempData) WHERE n.created < timestamp() - 2592000000 DETACH DELETE n
|
||||
|
||||
Return ONLY the Cypher DELETE statement.`,
|
||||
placeholder: 'Describe what you want to delete...',
|
||||
generationType: 'neo4j-cypher',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'cypherQuery',
|
||||
title: 'Cypher Query',
|
||||
type: 'code',
|
||||
placeholder: 'MATCH (n:Person) RETURN n LIMIT 10',
|
||||
required: true,
|
||||
condition: { field: 'operation', value: 'execute' },
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
maintainHistory: true,
|
||||
prompt: `You are an expert Neo4j developer. Generate any Cypher query based on the user's request.
|
||||
|
||||
### CONTEXT
|
||||
{context}
|
||||
|
||||
### CRITICAL INSTRUCTION
|
||||
Return ONLY the Cypher query. No explanations, no markdown, just the raw Cypher query.
|
||||
|
||||
### ADVANCED PATTERNS
|
||||
|
||||
**Aggregation**:
|
||||
MATCH (p:Person) RETURN p.country, count(p) as total ORDER BY total DESC
|
||||
|
||||
**Aggregation with COLLECT**:
|
||||
MATCH (p:Person)-[:WORKS_AT]->(c:Company) RETURN c.name, collect(p.name) as employees
|
||||
|
||||
**Complex Relationships**:
|
||||
MATCH (p:Person)-[:ACTED_IN]->(m:Movie)<-[:DIRECTED]-(d:Person) RETURN p.name, m.title, d.name
|
||||
|
||||
**WITH Clause for Chaining**:
|
||||
MATCH (p:Person) WITH p ORDER BY p.age DESC LIMIT 10 MATCH (p)-[:KNOWS]->(friend) RETURN p.name, collect(friend.name) as friends
|
||||
|
||||
**Conditional Logic (CASE)**:
|
||||
MATCH (p:Person) RETURN p.name, CASE WHEN p.age < 18 THEN 'minor' WHEN p.age < 65 THEN 'adult' ELSE 'senior' END as ageGroup
|
||||
|
||||
**Subqueries with EXISTS**:
|
||||
MATCH (p:Person) WHERE EXISTS { MATCH (p)-[:KNOWS]->(:Person)-[:KNOWS]->(:Person) } RETURN p
|
||||
|
||||
**Subqueries with CALL**:
|
||||
MATCH (p:Person) CALL { WITH p MATCH (p)-[:KNOWS]->(friend) RETURN count(friend) as friendCount } RETURN p.name, friendCount
|
||||
|
||||
**UNION (Combine Results)**:
|
||||
MATCH (p:Person) WHERE p.age > 30 RETURN p.name, p.age UNION MATCH (p:Person) WHERE p.country = "USA" RETURN p.name, p.age
|
||||
|
||||
**FOREACH for Batch Updates**:
|
||||
MATCH (p:Person) WHERE p.status = 'pending' FOREACH (x IN CASE WHEN p.age > 18 THEN [1] ELSE [] END | SET p.verified = true)
|
||||
|
||||
**Path Patterns**:
|
||||
MATCH path = (p1:Person)-[:KNOWS*1..3]-(p2:Person) WHERE p1.name = "Alice" RETURN path, length(path)
|
||||
|
||||
**Transaction Batching** (5.x+):
|
||||
CALL { MATCH (p:Person) WHERE p.status = 'inactive' DETACH DELETE p } IN TRANSACTIONS OF 100 ROWS
|
||||
|
||||
**Optional Match**:
|
||||
MATCH (p:Person) OPTIONAL MATCH (p)-[:MANAGES]->(e:Person) RETURN p.name, collect(e.name) as employees
|
||||
|
||||
**Map Projections**:
|
||||
MATCH (p:Person) RETURN p { .name, .age, friendCount: size((p)-[:KNOWS]->()) }
|
||||
|
||||
**Parameters in Advanced Query**:
|
||||
MATCH (p:Person) WHERE p.age > $minAge WITH p ORDER BY p.age DESC LIMIT $limit MATCH (p)-[:KNOWS]->(friend) RETURN p.name, collect(friend.name) as friends
|
||||
|
||||
**Pattern Comprehension**:
|
||||
MATCH (p:Person) RETURN p.name, [(p)-[:KNOWS]->(friend) WHERE friend.age > 25 | friend.name] as adultFriends
|
||||
|
||||
Return ONLY the Cypher query.`,
|
||||
placeholder: 'Describe your query...',
|
||||
generationType: 'neo4j-cypher',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'parameters',
|
||||
title: 'Parameters',
|
||||
type: 'code',
|
||||
placeholder: '{"name": "Alice", "minAge": 21}',
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
maintainHistory: true,
|
||||
prompt: `Generate JSON parameters for Cypher queries. Parameters are essential for security, performance, and reusability.
|
||||
|
||||
### CONTEXT
|
||||
{context}
|
||||
|
||||
### CRITICAL INSTRUCTION
|
||||
Return ONLY a valid JSON object with parameter values. No explanations, no markdown, just the raw JSON object.
|
||||
|
||||
### WHY USE PARAMETERS?
|
||||
|
||||
**Security Benefits:**
|
||||
- Prevents Cypher injection attacks
|
||||
- Safely handles user input
|
||||
- Separates data from query logic
|
||||
- No need to escape special characters
|
||||
|
||||
**Performance Benefits:**
|
||||
- Neo4j caches query execution plans
|
||||
- Same query structure with different parameters reuses cached plan
|
||||
- Significantly faster for repeated queries
|
||||
|
||||
**Code Quality:**
|
||||
- Cleaner, more readable queries
|
||||
- Easier to test and maintain
|
||||
- Reusable query templates
|
||||
|
||||
### PARAMETER SYNTAX IN QUERIES
|
||||
|
||||
In Cypher queries, parameters are prefixed with $ and referenced by name:
|
||||
|
||||
MATCH (n:Person {name: $name}) WHERE n.age > $minAge RETURN n
|
||||
MATCH (p)-[r:KNOWS {since: $year}]->(f) RETURN p, f
|
||||
CREATE (n:Person {name: $name, email: $email, age: $age})
|
||||
MERGE (n:User {id: $userId}) ON CREATE SET n.created = $timestamp
|
||||
|
||||
### DATA TYPES
|
||||
|
||||
**Strings**:
|
||||
{"name": "Alice", "email": "alice@example.com", "status": "active"}
|
||||
|
||||
**Numbers** (integers and floats):
|
||||
{"age": 30, "score": 95.5, "count": 1000, "rating": 4.8}
|
||||
|
||||
**Booleans**:
|
||||
{"isActive": true, "verified": false, "premium": true}
|
||||
|
||||
**Arrays**:
|
||||
{"tags": ["neo4j", "database", "graph"], "statuses": ["active", "pending"], "scores": [85, 90, 95]}
|
||||
|
||||
**Null**:
|
||||
{"middleName": null, "company": null}
|
||||
|
||||
**Mixed Types**:
|
||||
{"name": "Alice", "age": 30, "active": true, "tags": ["user", "premium"], "balance": 150.50}
|
||||
|
||||
### QUERY + PARAMETERS EXAMPLES
|
||||
|
||||
**Example 1: Simple Property Match**
|
||||
Query: MATCH (n:Person {email: $email}) RETURN n
|
||||
Parameters: {"email": "alice@example.com"}
|
||||
|
||||
**Example 2: Range Filter**
|
||||
Query: MATCH (n:Person) WHERE n.age >= $minAge AND n.age <= $maxAge RETURN n
|
||||
Parameters: {"minAge": 21, "maxAge": 65}
|
||||
|
||||
**Example 3: Array Membership**
|
||||
Query: MATCH (n:Person) WHERE n.status IN $statuses RETURN n
|
||||
Parameters: {"statuses": ["active", "pending", "verified"]}
|
||||
|
||||
**Example 4: Create with Parameters**
|
||||
Query: CREATE (n:Person {name: $name, email: $email, age: $age, created: $timestamp})
|
||||
Parameters: {"name": "Bob", "email": "bob@example.com", "age": 28, "timestamp": 1704067200000}
|
||||
|
||||
**Example 5: Update with Parameters**
|
||||
Query: MATCH (n:Person {id: $userId}) SET n.lastLogin = $loginTime, n.loginCount = n.loginCount + 1
|
||||
Parameters: {"userId": "user123", "loginTime": 1704067200000}
|
||||
|
||||
**Example 6: Relationship with Parameters**
|
||||
Query: MATCH (a:Person {id: $fromId}), (b:Person {id: $toId}) CREATE (a)-[:KNOWS {since: $year}]->(b)
|
||||
Parameters: {"fromId": "user1", "toId": "user2", "year": 2024}
|
||||
|
||||
### LIMITATIONS
|
||||
|
||||
**Cannot be Parameterized:**
|
||||
- Label names: Cannot use $labelName
|
||||
- Relationship types: Cannot use $relType
|
||||
- Property keys: Cannot use $propertyKey
|
||||
- Keywords: Cannot parameterize MATCH, CREATE, etc.
|
||||
|
||||
**Can be Parameterized:**
|
||||
- Property values ✓
|
||||
- Numeric constants ✓
|
||||
- String literals ✓
|
||||
- Arrays and lists ✓
|
||||
- WHERE clause conditions ✓
|
||||
|
||||
### BEST PRACTICES
|
||||
|
||||
1. **Always use parameters for user input** - Never concatenate user data into queries
|
||||
2. **Use descriptive names** - $userId instead of $id, $minAge instead of $min
|
||||
3. **Type appropriately** - Use numbers for numeric values, not strings
|
||||
4. **Validate before passing** - Ensure data types match expected values
|
||||
5. **Reuse parameter names** - Same parameters work across similar queries for plan caching
|
||||
|
||||
Return ONLY valid JSON.`,
|
||||
placeholder: 'Describe the parameter values...',
|
||||
generationType: 'neo4j-parameters',
|
||||
},
|
||||
},
|
||||
],
|
||||
tools: {
|
||||
access: [
|
||||
'neo4j_query',
|
||||
'neo4j_create',
|
||||
'neo4j_merge',
|
||||
'neo4j_update',
|
||||
'neo4j_delete',
|
||||
'neo4j_execute',
|
||||
],
|
||||
config: {
|
||||
tool: (params) => {
|
||||
switch (params.operation) {
|
||||
case 'query':
|
||||
return 'neo4j_query'
|
||||
case 'create':
|
||||
return 'neo4j_create'
|
||||
case 'merge':
|
||||
return 'neo4j_merge'
|
||||
case 'update':
|
||||
return 'neo4j_update'
|
||||
case 'delete':
|
||||
return 'neo4j_delete'
|
||||
case 'execute':
|
||||
return 'neo4j_execute'
|
||||
default:
|
||||
throw new Error(`Invalid Neo4j operation: ${params.operation}`)
|
||||
}
|
||||
},
|
||||
params: (params) => {
|
||||
const { operation, parameters, ...rest } = params
|
||||
|
||||
let parsedParameters
|
||||
if (typeof parameters === 'string') {
|
||||
const trimmed = parameters.trim()
|
||||
if (trimmed === '') {
|
||||
parsedParameters = undefined
|
||||
} else {
|
||||
try {
|
||||
parsedParameters = JSON.parse(trimmed)
|
||||
} catch (parseError) {
|
||||
const errorMsg =
|
||||
parseError instanceof Error ? parseError.message : 'Unknown JSON error'
|
||||
throw new Error(
|
||||
`Invalid JSON parameters format: ${errorMsg}. Please check your JSON syntax.`
|
||||
)
|
||||
}
|
||||
}
|
||||
} else if (parameters && typeof parameters === 'object') {
|
||||
parsedParameters = parameters
|
||||
} else {
|
||||
parsedParameters = undefined
|
||||
}
|
||||
|
||||
const connectionConfig = {
|
||||
host: rest.host,
|
||||
port: typeof rest.port === 'string' ? Number.parseInt(rest.port, 10) : rest.port || 7687,
|
||||
database: rest.database || 'neo4j',
|
||||
username: rest.username || 'neo4j',
|
||||
password: rest.password,
|
||||
encryption: rest.encryption || 'disabled',
|
||||
}
|
||||
|
||||
const result: any = { ...connectionConfig }
|
||||
|
||||
if (rest.cypherQuery) {
|
||||
result.cypherQuery = rest.cypherQuery
|
||||
}
|
||||
|
||||
if (parsedParameters !== undefined && parsedParameters !== null) {
|
||||
result.parameters = parsedParameters
|
||||
} else {
|
||||
result.parameters = undefined
|
||||
}
|
||||
|
||||
if (rest.detach !== undefined) {
|
||||
result.detach = rest.detach === 'true' || rest.detach === true
|
||||
}
|
||||
|
||||
return result
|
||||
},
|
||||
},
|
||||
},
|
||||
inputs: {
|
||||
operation: { type: 'string', description: 'Database operation to perform' },
|
||||
host: { type: 'string', description: 'Neo4j host' },
|
||||
port: { type: 'string', description: 'Neo4j port (Bolt protocol)' },
|
||||
database: { type: 'string', description: 'Database name' },
|
||||
username: { type: 'string', description: 'Neo4j username' },
|
||||
password: { type: 'string', description: 'Neo4j password' },
|
||||
encryption: { type: 'string', description: 'Connection encryption mode' },
|
||||
cypherQuery: { type: 'string', description: 'Cypher query to execute' },
|
||||
parameters: { type: 'json', description: 'Query parameters as JSON object' },
|
||||
detach: { type: 'boolean', description: 'Use DETACH DELETE for delete operations' },
|
||||
},
|
||||
outputs: {
|
||||
message: {
|
||||
type: 'string',
|
||||
description: 'Success or error message describing the operation outcome',
|
||||
},
|
||||
records: {
|
||||
type: 'array',
|
||||
description: 'Array of records returned from the query',
|
||||
},
|
||||
recordCount: {
|
||||
type: 'number',
|
||||
description: 'Number of records returned or affected',
|
||||
},
|
||||
summary: {
|
||||
type: 'json',
|
||||
description: 'Execution summary with timing and database change counters',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -10,6 +10,7 @@ export const ScheduleBlock: BlockConfig = {
|
||||
triggerAllowed: true,
|
||||
name: 'Schedule',
|
||||
description: 'Trigger workflow execution on a schedule',
|
||||
docsLink: 'https://docs.sim.ai/triggers/schedule',
|
||||
longDescription:
|
||||
'Integrate Schedule into the workflow. Can trigger a workflow on a schedule configuration.',
|
||||
bestPractices: `
|
||||
|
||||
@@ -41,6 +41,7 @@ export const WebhookBlock: BlockConfig = {
|
||||
category: 'triggers',
|
||||
icon: WebhookIcon,
|
||||
bgColor: '#10B981', // Green color for triggers
|
||||
docsLink: 'https://docs.sim.ai/triggers/webhook',
|
||||
triggerAllowed: true,
|
||||
hideFromToolbar: true, // Hidden for backwards compatibility - use generic webhook trigger instead
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import { ApolloBlock } from '@/blocks/blocks/apollo'
|
||||
import { ArxivBlock } from '@/blocks/blocks/arxiv'
|
||||
import { AsanaBlock } from '@/blocks/blocks/asana'
|
||||
import { BrowserUseBlock } from '@/blocks/blocks/browser_use'
|
||||
import { CalendlyBlock } from '@/blocks/blocks/calendly'
|
||||
import { ChatTriggerBlock } from '@/blocks/blocks/chat_trigger'
|
||||
import { ClayBlock } from '@/blocks/blocks/clay'
|
||||
import { ConditionBlock } from '@/blocks/blocks/condition'
|
||||
@@ -49,6 +50,7 @@ import { MicrosoftTeamsBlock } from '@/blocks/blocks/microsoft_teams'
|
||||
import { MistralParseBlock } from '@/blocks/blocks/mistral_parse'
|
||||
import { MongoDBBlock } from '@/blocks/blocks/mongodb'
|
||||
import { MySQLBlock } from '@/blocks/blocks/mysql'
|
||||
import { Neo4jBlock } from '@/blocks/blocks/neo4j'
|
||||
import { NoteBlock } from '@/blocks/blocks/note'
|
||||
import { NotionBlock } from '@/blocks/blocks/notion'
|
||||
import { OneDriveBlock } from '@/blocks/blocks/onedrive'
|
||||
@@ -108,6 +110,7 @@ export const registry: Record<string, BlockConfig> = {
|
||||
arxiv: ArxivBlock,
|
||||
asana: AsanaBlock,
|
||||
browser_use: BrowserUseBlock,
|
||||
calendly: CalendlyBlock,
|
||||
clay: ClayBlock,
|
||||
condition: ConditionBlock,
|
||||
confluence: ConfluenceBlock,
|
||||
@@ -148,6 +151,7 @@ export const registry: Record<string, BlockConfig> = {
|
||||
mistral_parse: MistralParseBlock,
|
||||
mongodb: MongoDBBlock,
|
||||
mysql: MySQLBlock,
|
||||
neo4j: Neo4jBlock,
|
||||
note: NoteBlock,
|
||||
notion: NotionBlock,
|
||||
openai: OpenAIBlock,
|
||||
|
||||
@@ -35,6 +35,8 @@ export type GenerationType =
|
||||
| 'mongodb-sort'
|
||||
| 'mongodb-documents'
|
||||
| 'mongodb-update'
|
||||
| 'neo4j-cypher'
|
||||
| 'neo4j-parameters'
|
||||
|
||||
export type SubBlockType =
|
||||
| 'short-input' // Single line input
|
||||
|
||||
@@ -4032,3 +4032,55 @@ export function ApolloIcon(props: SVGProps<SVGSVGElement>) {
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function Neo4jIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} viewBox='0 0 128 128' fill='currentColor' xmlns='http://www.w3.org/2000/svg'>
|
||||
<path
|
||||
d='M63.333 32.567c-5.2.866-9.566 3-12.833 6.266-3.867 3.867-5.833 8.5-6.5 15.367-.3 3.133-.467 15.467-.2 15.467.067 0 .7-.234 1.4-.534 1.633-.7 5.167-.7 7-.033l1.4.5.167-8.033c.166-8.567.366-9.867 1.966-13.067 1.1-2.133 3.767-4.633 6.034-5.667 2.6-1.2 6.4-1.666 9.333-1.2 6.267 1.034 10 4.434 11.567 10.5.633 2.434.666 3.7.666 17.1v14.434H93.4L93.233 67.9c-.1-14.9-.166-15.9-.866-18.567-1.9-7.4-6.5-12.766-12.934-15.2-3.433-1.3-6.7-1.8-11.2-1.766-2.233.033-4.433.133-4.9.2z'
|
||||
fill='#000'
|
||||
/>
|
||||
<path
|
||||
d='M22.733 57.2c-2.866 1.433-4.4 4-4.4 7.467 0 1.1.2 2.5.467 3.133.633 1.567 2.433 3.467 4 4.3 1.9 1 5.5 1 7.367.033l1.366-.7 4.267 2.9 4.267 2.934V81.7L35.8 84.633l-4.3 2.934-1.1-.667c-1.6-.933-4.7-1.133-6.6-.4-2 .767-4.067 2.6-4.833 4.333-.834 1.767-.834 5.234 0 7 .7 1.567 2.333 3.3 3.8 4.067.6.3 2.033.6 3.233.7 2.8.2 5.167-.733 6.867-2.733 1.366-1.6 2.266-4.4 2.033-6.334l-.167-1.366 4.3-2.9 4.3-2.9 1.534.7c2.333 1 5.8.766 8-.567 2.4-1.5 3.6-3.633 3.733-6.633.1-2.1 0-2.567-.833-4.2-2.167-4.134-7-5.7-11.134-3.634l-1.233.6-4.233-2.9-4.234-2.9-.1-2.333c-.066-2.8-.866-4.6-2.833-6.233-2.5-2.134-6.233-2.567-9.267-1.067z'
|
||||
fill='#018BFF'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function CalendlyIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
viewBox='0 0 512 512'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
fillRule='evenodd'
|
||||
clipRule='evenodd'
|
||||
strokeLinejoin='round'
|
||||
strokeMiterlimit='2'
|
||||
>
|
||||
<g fillRule='nonzero'>
|
||||
<path
|
||||
d='M346.955 330.224c-15.875 14.088-35.7 31.619-71.647 31.619h-21.495c-26.012 0-49.672-9.455-66.607-26.593-16.543-16.747-25.649-39.665-25.649-64.545v-29.41c0-24.88 9.106-47.799 25.65-64.545 16.934-17.138 40.594-26.579 66.606-26.579h21.495c35.99 0 55.772 17.516 71.647 31.604 16.484 14.524 30.703 27.218 68.625 27.218a109.162 109.162 0 0017.269-1.38l-.13-.334a129.909 129.909 0 00-7.974-16.382L399.4 146.99c-23.232-40.234-66.304-65.098-112.763-65.096h-50.703c-46.46-.002-89.531 24.862-112.764 65.096l-25.344 43.906c-23.224 40.238-23.224 89.968 0 130.206l25.344 43.906c23.233 40.234 66.305 65.098 112.764 65.096h50.703c46.459.002 89.53-24.862 112.763-65.096l25.345-43.833a129.909 129.909 0 007.973-16.383l.13-.32a107.491 107.491 0 00-17.268-1.452c-37.922 0-52.14 12.621-68.625 27.218'
|
||||
fill='#006bff'
|
||||
/>
|
||||
<path
|
||||
d='M275.308 176.823h-21.495c-39.592 0-65.605 28.278-65.605 64.471v29.411c0 36.194 26.013 64.472 65.605 64.472h21.495c57.69 0 53.158-58.822 140.272-58.822 8.254-.009 16.49.75 24.603 2.266a130.047 130.047 0 000-45.242 134.431 134.431 0 01-24.603 2.266c-87.143 0-82.583-58.822-140.272-58.822'
|
||||
fill='#006bff'
|
||||
/>
|
||||
<path
|
||||
d='M490.233 300.116a121.451 121.451 0 00-50.035-21.51v.436a130.296 130.296 0 01-7.262 25.344 95.25 95.25 0 0141.364 17.037c0 .116-.072.261-.116.392-28.788 93.217-115.55 157.228-213.112 157.228-122.358 0-223.044-100.685-223.044-223.043S138.714 32.956 261.072 32.956c97.561 0 184.324 64.012 213.112 157.229 0 .13.073.276.116.392a95.073 95.073 0 01-41.364 17.022 131.112 131.112 0 017.262 25.373 3.166 3.166 0 000 .407 121.415 121.415 0 0050.035-21.495c14.262-10.56 11.503-22.483 9.339-29.542C467.34 77.803 370.064 6 260.67 6c-137.147 0-250 112.854-250 250 0 137.146 112.853 250 250 250 109.394 0 206.67-71.803 238.902-176.342 2.164-7.059 4.923-18.983-9.34-29.542'
|
||||
fill='#006bff'
|
||||
/>
|
||||
<path
|
||||
d='M432.849 207.599a107.491 107.491 0 01-17.269 1.452c-37.922 0-52.14-12.62-68.61-27.217-15.89-14.089-35.672-31.619-71.662-31.619h-21.495c-26.027 0-49.672 9.455-66.607 26.593-16.543 16.746-25.649 39.665-25.649 64.545v29.41c0 24.88 9.106 47.799 25.65 64.545 16.934 17.138 40.579 26.578 66.606 26.578h21.495c35.99 0 55.772-17.515 71.661-31.604 16.47-14.524 30.69-27.217 68.611-27.217 5.783.001 11.558.463 17.269 1.38a129.303 129.303 0 007.262-25.345c.009-.145.009-.29 0-.436a134.301 134.301 0 00-24.604-2.25c-87.143 0-82.583 58.836-140.271 58.836H253.74c-39.592 0-65.604-28.293-65.604-64.487v-29.469c0-36.193 26.012-64.471 65.604-64.471h21.496c57.688 0 53.157 58.807 140.271 58.807 8.254.015 16.49-.74 24.604-2.251v-.407a131.112 131.112 0 00-7.262-25.373'
|
||||
fill='#0ae8f0'
|
||||
/>
|
||||
<path
|
||||
d='M432.849 207.599a107.491 107.491 0 01-17.269 1.452c-37.922 0-52.14-12.62-68.61-27.217-15.89-14.089-35.672-31.619-71.662-31.619h-21.495c-26.027 0-49.672 9.455-66.607 26.593-16.543 16.746-25.649 39.665-25.649 64.545v29.41c0 24.88 9.106 47.799 25.65 64.545 16.934 17.138 40.579 26.578 66.606 26.578h21.495c35.99 0 55.772-17.515 71.661-31.604 16.47-14.524 30.69-27.217 68.611-27.217 5.783.001 11.558.463 17.269 1.38a129.303 129.303 0 007.262-25.345c.009-.145.009-.29 0-.436a134.301 134.301 0 00-24.604-2.25c-87.143 0-82.583 58.836-140.271 58.836H253.74c-39.592 0-65.604-28.293-65.604-64.487v-29.469c0-36.193 26.012-64.471 65.604-64.471h21.496c57.688 0 53.157 58.807 140.271 58.807 8.254.015 16.49-.74 24.604-2.251v-.407a131.112 131.112 0 00-7.262-25.373'
|
||||
fill='#0ae8f0'
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ export function TagInput({
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex min-h-[2.5rem] flex-wrap gap-1.5 rounded-md border border-input bg-background p-2',
|
||||
'flex min-h-[34px] flex-wrap gap-1 rounded-[4px] border border-[var(--surface-11)] bg-[var(--surface-6)] px-[8px] py-[6px] dark:bg-[var(--surface-9)]',
|
||||
disabled && 'cursor-not-allowed opacity-50',
|
||||
className
|
||||
)}
|
||||
@@ -68,9 +68,9 @@ export function TagInput({
|
||||
<Badge
|
||||
key={tag}
|
||||
variant='secondary'
|
||||
className='h-7 gap-1.5 border-0 bg-muted/60 pr-1.5 pl-2.5 hover:bg-muted/80'
|
||||
className='h-[22px] gap-1 border-0 bg-muted/60 pr-1 pl-2 text-xs hover:bg-muted/80'
|
||||
>
|
||||
<span className='text-xs'>{tag}</span>
|
||||
<span>{tag}</span>
|
||||
{!disabled && (
|
||||
<button
|
||||
type='button'
|
||||
@@ -95,7 +95,7 @@ export function TagInput({
|
||||
onBlur={handleBlur}
|
||||
placeholder={value.length === 0 ? placeholder : ''}
|
||||
disabled={disabled}
|
||||
className='h-7 min-w-[120px] flex-1 border-0 bg-transparent p-0 px-1 text-sm shadow-none placeholder:text-muted-foreground/60 focus-visible:ring-0 focus-visible:ring-offset-0'
|
||||
className='h-[22px] min-w-[120px] flex-1 border-0 bg-transparent p-0 text-sm shadow-none placeholder:text-muted-foreground/60 focus-visible:ring-0 focus-visible:ring-offset-0'
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1337,6 +1337,27 @@ export async function formatWebhookInput(
|
||||
}
|
||||
}
|
||||
|
||||
if (foundWebhook.provider === 'calendly') {
|
||||
// Calendly webhook payload format matches the trigger outputs
|
||||
return {
|
||||
event: body.event,
|
||||
created_at: body.created_at,
|
||||
created_by: body.created_by,
|
||||
payload: body.payload,
|
||||
webhook: {
|
||||
data: {
|
||||
provider: 'calendly',
|
||||
path: foundWebhook.path,
|
||||
providerConfig: foundWebhook.providerConfig,
|
||||
payload: body,
|
||||
headers: Object.fromEntries(request.headers.entries()),
|
||||
method: request.method,
|
||||
},
|
||||
},
|
||||
workflowId: foundWorkflow.id,
|
||||
}
|
||||
}
|
||||
|
||||
// Generic format for other providers
|
||||
return {
|
||||
webhook: {
|
||||
|
||||
@@ -7,6 +7,7 @@ const teamsLogger = createLogger('TeamsSubscription')
|
||||
const telegramLogger = createLogger('TelegramWebhook')
|
||||
const airtableLogger = createLogger('AirtableWebhook')
|
||||
const typeformLogger = createLogger('TypeformWebhook')
|
||||
const calendlyLogger = createLogger('CalendlyWebhook')
|
||||
|
||||
function getProviderConfig(webhook: any): Record<string, any> {
|
||||
return (webhook.providerConfig as Record<string, any>) || {}
|
||||
@@ -611,9 +612,58 @@ export async function deleteTypeformWebhook(webhook: any, requestId: string): Pr
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a Calendly webhook subscription
|
||||
* Don't fail webhook deletion if cleanup fails
|
||||
*/
|
||||
export async function deleteCalendlyWebhook(webhook: any, requestId: string): Promise<void> {
|
||||
try {
|
||||
const config = getProviderConfig(webhook)
|
||||
const apiKey = config.apiKey as string | undefined
|
||||
const externalId = config.externalId as string | undefined
|
||||
|
||||
if (!apiKey) {
|
||||
calendlyLogger.warn(
|
||||
`[${requestId}] Missing apiKey for Calendly webhook deletion ${webhook.id}, skipping cleanup`
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (!externalId) {
|
||||
calendlyLogger.warn(
|
||||
`[${requestId}] Missing externalId for Calendly webhook deletion ${webhook.id}, skipping cleanup`
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const calendlyApiUrl = `https://api.calendly.com/webhook_subscriptions/${externalId}`
|
||||
|
||||
const calendlyResponse = await fetch(calendlyApiUrl, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
},
|
||||
})
|
||||
|
||||
if (!calendlyResponse.ok && calendlyResponse.status !== 404) {
|
||||
const responseBody = await calendlyResponse.json().catch(() => ({}))
|
||||
calendlyLogger.warn(
|
||||
`[${requestId}] Failed to delete Calendly webhook (non-fatal): ${calendlyResponse.status}`,
|
||||
{ response: responseBody }
|
||||
)
|
||||
} else {
|
||||
calendlyLogger.info(
|
||||
`[${requestId}] Successfully deleted Calendly webhook subscription ${externalId}`
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
calendlyLogger.warn(`[${requestId}] Error deleting Calendly webhook (non-fatal)`, error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up external webhook subscriptions for a webhook
|
||||
* Handles Airtable, Teams, Telegram, and Typeform cleanup
|
||||
* Handles Airtable, Teams, Telegram, Typeform, and Calendly cleanup
|
||||
* Don't fail deletion if cleanup fails
|
||||
*/
|
||||
export async function cleanupExternalWebhook(
|
||||
@@ -629,5 +679,7 @@ export async function cleanupExternalWebhook(
|
||||
await deleteTelegramWebhook(webhook, requestId)
|
||||
} else if (webhook.provider === 'typeform') {
|
||||
await deleteTypeformWebhook(webhook, requestId)
|
||||
} else if (webhook.provider === 'calendly') {
|
||||
await deleteCalendlyWebhook(webhook, requestId)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,6 +75,7 @@
|
||||
"entities": "6.0.1",
|
||||
"framer-motion": "^12.5.0",
|
||||
"fuse.js": "7.1.0",
|
||||
"fuzzysort": "3.1.0",
|
||||
"gray-matter": "^4.0.3",
|
||||
"groq-sdk": "^0.15.0",
|
||||
"html-to-text": "^9.0.5",
|
||||
|
||||
87
apps/sim/tools/calendly/cancel_event.ts
Normal file
87
apps/sim/tools/calendly/cancel_event.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import type { CalendlyCancelEventParams, CalendlyCancelEventResponse } from '@/tools/calendly/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const cancelEventTool: ToolConfig<CalendlyCancelEventParams, CalendlyCancelEventResponse> = {
|
||||
id: 'calendly_cancel_event',
|
||||
name: 'Calendly Cancel Event',
|
||||
description: 'Cancel a scheduled event',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Calendly Personal Access Token',
|
||||
},
|
||||
eventUuid: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Scheduled event UUID to cancel (can be full URI or just the UUID)',
|
||||
},
|
||||
reason: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Reason for cancellation (will be sent to invitees)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: CalendlyCancelEventParams) => {
|
||||
const uuid = params.eventUuid.includes('/')
|
||||
? params.eventUuid.split('/').pop()
|
||||
: params.eventUuid
|
||||
return `https://api.calendly.com/scheduled_events/${uuid}/cancellation`
|
||||
},
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params: CalendlyCancelEventParams) => {
|
||||
const body: any = {}
|
||||
|
||||
if (params.reason) {
|
||||
body.reason = params.reason
|
||||
}
|
||||
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: data,
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
resource: {
|
||||
type: 'object',
|
||||
description: 'Cancellation details',
|
||||
properties: {
|
||||
canceler_type: {
|
||||
type: 'string',
|
||||
description: 'Type of canceler (host or invitee)',
|
||||
},
|
||||
canceled_by: {
|
||||
type: 'string',
|
||||
description: 'Name of person who canceled',
|
||||
},
|
||||
reason: {
|
||||
type: 'string',
|
||||
description: 'Cancellation reason',
|
||||
},
|
||||
created_at: {
|
||||
type: 'string',
|
||||
description: 'ISO timestamp when event was canceled',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
151
apps/sim/tools/calendly/create_webhook.ts
Normal file
151
apps/sim/tools/calendly/create_webhook.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
import type {
|
||||
CalendlyCreateWebhookParams,
|
||||
CalendlyCreateWebhookResponse,
|
||||
} from '@/tools/calendly/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const createWebhookTool: ToolConfig<
|
||||
CalendlyCreateWebhookParams,
|
||||
CalendlyCreateWebhookResponse
|
||||
> = {
|
||||
id: 'calendly_create_webhook',
|
||||
name: 'Calendly Create Webhook',
|
||||
description: 'Create a new webhook subscription to receive real-time event notifications',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Calendly Personal Access Token',
|
||||
},
|
||||
url: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'URL to receive webhook events (must be HTTPS)',
|
||||
},
|
||||
events: {
|
||||
type: 'json',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Array of event types to subscribe to (e.g., ["invitee.created", "invitee.canceled"])',
|
||||
},
|
||||
organization: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Organization URI',
|
||||
},
|
||||
user: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'User URI (required for user-scoped webhooks)',
|
||||
},
|
||||
scope: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Webhook scope: "organization" or "user"',
|
||||
},
|
||||
signing_key: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Optional signing key to verify webhook signatures',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: () => 'https://api.calendly.com/webhook_subscriptions',
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params: CalendlyCreateWebhookParams) => {
|
||||
const body: any = {
|
||||
url: params.url,
|
||||
events: params.events,
|
||||
organization: params.organization,
|
||||
scope: params.scope,
|
||||
}
|
||||
|
||||
if (params.user && params.scope === 'user') {
|
||||
body.user = params.user
|
||||
}
|
||||
|
||||
if (params.signing_key) {
|
||||
body.signing_key = params.signing_key
|
||||
}
|
||||
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: data,
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
resource: {
|
||||
type: 'object',
|
||||
description: 'Created webhook subscription details',
|
||||
properties: {
|
||||
uri: {
|
||||
type: 'string',
|
||||
description: 'Canonical reference to the webhook',
|
||||
},
|
||||
callback_url: {
|
||||
type: 'string',
|
||||
description: 'URL receiving webhook events',
|
||||
},
|
||||
created_at: {
|
||||
type: 'string',
|
||||
description: 'ISO timestamp of creation',
|
||||
},
|
||||
updated_at: {
|
||||
type: 'string',
|
||||
description: 'ISO timestamp of last update',
|
||||
},
|
||||
state: {
|
||||
type: 'string',
|
||||
description: 'Webhook state (active by default)',
|
||||
},
|
||||
events: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
description: 'Subscribed event types',
|
||||
},
|
||||
signing_key: {
|
||||
type: 'string',
|
||||
description: 'Key to verify webhook signatures',
|
||||
},
|
||||
scope: {
|
||||
type: 'string',
|
||||
description: 'Webhook scope',
|
||||
},
|
||||
organization: {
|
||||
type: 'string',
|
||||
description: 'Organization URI',
|
||||
},
|
||||
user: {
|
||||
type: 'string',
|
||||
description: 'User URI (for user-scoped webhooks)',
|
||||
},
|
||||
creator: {
|
||||
type: 'string',
|
||||
description: 'URI of user who created the webhook',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
76
apps/sim/tools/calendly/delete_webhook.ts
Normal file
76
apps/sim/tools/calendly/delete_webhook.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import type {
|
||||
CalendlyDeleteWebhookParams,
|
||||
CalendlyDeleteWebhookResponse,
|
||||
} from '@/tools/calendly/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const deleteWebhookTool: ToolConfig<
|
||||
CalendlyDeleteWebhookParams,
|
||||
CalendlyDeleteWebhookResponse
|
||||
> = {
|
||||
id: 'calendly_delete_webhook',
|
||||
name: 'Calendly Delete Webhook',
|
||||
description: 'Delete a webhook subscription',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Calendly Personal Access Token',
|
||||
},
|
||||
webhookUuid: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Webhook subscription UUID to delete (can be full URI or just the UUID)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: CalendlyDeleteWebhookParams) => {
|
||||
const uuid = params.webhookUuid.includes('/')
|
||||
? params.webhookUuid.split('/').pop()
|
||||
: params.webhookUuid
|
||||
return `https://api.calendly.com/webhook_subscriptions/${uuid}`
|
||||
},
|
||||
method: 'DELETE',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (response.status === 204 || response.status === 200) {
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
deleted: true,
|
||||
message: 'Webhook subscription deleted successfully',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: false,
|
||||
output: {
|
||||
deleted: false,
|
||||
message: data.message || 'Failed to delete webhook subscription',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
deleted: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the webhook was successfully deleted',
|
||||
},
|
||||
message: {
|
||||
type: 'string',
|
||||
description: 'Status message',
|
||||
},
|
||||
},
|
||||
}
|
||||
91
apps/sim/tools/calendly/get_current_user.ts
Normal file
91
apps/sim/tools/calendly/get_current_user.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import type {
|
||||
CalendlyGetCurrentUserParams,
|
||||
CalendlyGetCurrentUserResponse,
|
||||
} from '@/tools/calendly/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const getCurrentUserTool: ToolConfig<
|
||||
CalendlyGetCurrentUserParams,
|
||||
CalendlyGetCurrentUserResponse
|
||||
> = {
|
||||
id: 'calendly_get_current_user',
|
||||
name: 'Calendly Get Current User',
|
||||
description: 'Get information about the currently authenticated Calendly user',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Calendly Personal Access Token',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: () => 'https://api.calendly.com/users/me',
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: data,
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
resource: {
|
||||
type: 'object',
|
||||
description: 'Current user information',
|
||||
properties: {
|
||||
uri: {
|
||||
type: 'string',
|
||||
description: 'Canonical reference to the user',
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'User full name',
|
||||
},
|
||||
slug: {
|
||||
type: 'string',
|
||||
description: 'Unique identifier for the user in URLs',
|
||||
},
|
||||
email: {
|
||||
type: 'string',
|
||||
description: 'User email address',
|
||||
},
|
||||
scheduling_url: {
|
||||
type: 'string',
|
||||
description: "URL to the user's scheduling page",
|
||||
},
|
||||
timezone: {
|
||||
type: 'string',
|
||||
description: 'User timezone',
|
||||
},
|
||||
avatar_url: {
|
||||
type: 'string',
|
||||
description: 'URL to user avatar image',
|
||||
},
|
||||
created_at: {
|
||||
type: 'string',
|
||||
description: 'ISO timestamp when user was created',
|
||||
},
|
||||
updated_at: {
|
||||
type: 'string',
|
||||
description: 'ISO timestamp when user was last updated',
|
||||
},
|
||||
current_organization: {
|
||||
type: 'string',
|
||||
description: 'URI of current organization',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
97
apps/sim/tools/calendly/get_event_type.ts
Normal file
97
apps/sim/tools/calendly/get_event_type.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import type {
|
||||
CalendlyGetEventTypeParams,
|
||||
CalendlyGetEventTypeResponse,
|
||||
} from '@/tools/calendly/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const getEventTypeTool: ToolConfig<
|
||||
CalendlyGetEventTypeParams,
|
||||
CalendlyGetEventTypeResponse
|
||||
> = {
|
||||
id: 'calendly_get_event_type',
|
||||
name: 'Calendly Get Event Type',
|
||||
description: 'Get detailed information about a specific event type',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Calendly Personal Access Token',
|
||||
},
|
||||
eventTypeUuid: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Event type UUID (can be full URI or just the UUID)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: CalendlyGetEventTypeParams) => {
|
||||
const uuid = params.eventTypeUuid.includes('/')
|
||||
? params.eventTypeUuid.split('/').pop()
|
||||
: params.eventTypeUuid
|
||||
return `https://api.calendly.com/event_types/${uuid}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: data,
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
resource: {
|
||||
type: 'object',
|
||||
description: 'Event type details',
|
||||
properties: {
|
||||
uri: { type: 'string', description: 'Canonical reference to the event type' },
|
||||
name: { type: 'string', description: 'Event type name' },
|
||||
active: { type: 'boolean', description: 'Whether the event type is active' },
|
||||
booking_method: { type: 'string', description: 'Booking method' },
|
||||
color: { type: 'string', description: 'Hex color code' },
|
||||
created_at: { type: 'string', description: 'ISO timestamp of creation' },
|
||||
custom_questions: {
|
||||
type: 'array',
|
||||
description: 'Custom questions for invitees',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string', description: 'Question text' },
|
||||
type: {
|
||||
type: 'string',
|
||||
description: 'Question type (text, single_select, multi_select, etc.)',
|
||||
},
|
||||
position: { type: 'number', description: 'Question order' },
|
||||
enabled: { type: 'boolean', description: 'Whether question is enabled' },
|
||||
required: { type: 'boolean', description: 'Whether question is required' },
|
||||
answer_choices: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
description: 'Available answer choices',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
description_html: { type: 'string', description: 'HTML formatted description' },
|
||||
description_plain: { type: 'string', description: 'Plain text description' },
|
||||
duration: { type: 'number', description: 'Duration in minutes' },
|
||||
scheduling_url: { type: 'string', description: 'URL to scheduling page' },
|
||||
slug: { type: 'string', description: 'Unique identifier for URLs' },
|
||||
type: { type: 'string', description: 'Event type classification' },
|
||||
updated_at: { type: 'string', description: 'ISO timestamp of last update' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
112
apps/sim/tools/calendly/get_scheduled_event.ts
Normal file
112
apps/sim/tools/calendly/get_scheduled_event.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import type {
|
||||
CalendlyGetScheduledEventParams,
|
||||
CalendlyGetScheduledEventResponse,
|
||||
} from '@/tools/calendly/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const getScheduledEventTool: ToolConfig<
|
||||
CalendlyGetScheduledEventParams,
|
||||
CalendlyGetScheduledEventResponse
|
||||
> = {
|
||||
id: 'calendly_get_scheduled_event',
|
||||
name: 'Calendly Get Scheduled Event',
|
||||
description: 'Get detailed information about a specific scheduled event',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Calendly Personal Access Token',
|
||||
},
|
||||
eventUuid: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Scheduled event UUID (can be full URI or just the UUID)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: CalendlyGetScheduledEventParams) => {
|
||||
const uuid = params.eventUuid.includes('/')
|
||||
? params.eventUuid.split('/').pop()
|
||||
: params.eventUuid
|
||||
return `https://api.calendly.com/scheduled_events/${uuid}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: data,
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
resource: {
|
||||
type: 'object',
|
||||
description: 'Scheduled event details',
|
||||
properties: {
|
||||
uri: { type: 'string', description: 'Canonical reference to the event' },
|
||||
name: { type: 'string', description: 'Event name' },
|
||||
status: { type: 'string', description: 'Event status (active or canceled)' },
|
||||
start_time: { type: 'string', description: 'ISO timestamp of event start' },
|
||||
end_time: { type: 'string', description: 'ISO timestamp of event end' },
|
||||
event_type: { type: 'string', description: 'URI of the event type' },
|
||||
location: {
|
||||
type: 'object',
|
||||
description: 'Event location details',
|
||||
properties: {
|
||||
type: { type: 'string', description: 'Location type' },
|
||||
location: { type: 'string', description: 'Location description' },
|
||||
join_url: { type: 'string', description: 'URL to join online meeting' },
|
||||
},
|
||||
},
|
||||
invitees_counter: {
|
||||
type: 'object',
|
||||
description: 'Invitee count information',
|
||||
properties: {
|
||||
total: { type: 'number', description: 'Total number of invitees' },
|
||||
active: { type: 'number', description: 'Number of active invitees' },
|
||||
limit: { type: 'number', description: 'Maximum number of invitees' },
|
||||
},
|
||||
},
|
||||
event_memberships: {
|
||||
type: 'array',
|
||||
description: 'Event hosts/members',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
user: { type: 'string', description: 'User URI' },
|
||||
user_email: { type: 'string', description: 'User email' },
|
||||
user_name: { type: 'string', description: 'User name' },
|
||||
},
|
||||
},
|
||||
},
|
||||
event_guests: {
|
||||
type: 'array',
|
||||
description: 'Additional guests',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
email: { type: 'string', description: 'Guest email' },
|
||||
created_at: { type: 'string', description: 'When guest was added' },
|
||||
updated_at: { type: 'string', description: 'When guest info was updated' },
|
||||
},
|
||||
},
|
||||
},
|
||||
created_at: { type: 'string', description: 'ISO timestamp of event creation' },
|
||||
updated_at: { type: 'string', description: 'ISO timestamp of last update' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
21
apps/sim/tools/calendly/index.ts
Normal file
21
apps/sim/tools/calendly/index.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { cancelEventTool } from '@/tools/calendly/cancel_event'
|
||||
import { createWebhookTool } from '@/tools/calendly/create_webhook'
|
||||
import { deleteWebhookTool } from '@/tools/calendly/delete_webhook'
|
||||
import { getCurrentUserTool } from '@/tools/calendly/get_current_user'
|
||||
import { getEventTypeTool } from '@/tools/calendly/get_event_type'
|
||||
import { getScheduledEventTool } from '@/tools/calendly/get_scheduled_event'
|
||||
import { listEventInviteesTool } from '@/tools/calendly/list_event_invitees'
|
||||
import { listEventTypesTool } from '@/tools/calendly/list_event_types'
|
||||
import { listScheduledEventsTool } from '@/tools/calendly/list_scheduled_events'
|
||||
import { listWebhooksTool } from '@/tools/calendly/list_webhooks'
|
||||
|
||||
export const calendlyGetCurrentUserTool = getCurrentUserTool
|
||||
export const calendlyListEventTypesTool = listEventTypesTool
|
||||
export const calendlyGetEventTypeTool = getEventTypeTool
|
||||
export const calendlyListScheduledEventsTool = listScheduledEventsTool
|
||||
export const calendlyGetScheduledEventTool = getScheduledEventTool
|
||||
export const calendlyListEventInviteesTool = listEventInviteesTool
|
||||
export const calendlyCancelEventTool = cancelEventTool
|
||||
export const calendlyListWebhooksTool = listWebhooksTool
|
||||
export const calendlyCreateWebhookTool = createWebhookTool
|
||||
export const calendlyDeleteWebhookTool = deleteWebhookTool
|
||||
154
apps/sim/tools/calendly/list_event_invitees.ts
Normal file
154
apps/sim/tools/calendly/list_event_invitees.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
import type {
|
||||
CalendlyListEventInviteesParams,
|
||||
CalendlyListEventInviteesResponse,
|
||||
} from '@/tools/calendly/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const listEventInviteesTool: ToolConfig<
|
||||
CalendlyListEventInviteesParams,
|
||||
CalendlyListEventInviteesResponse
|
||||
> = {
|
||||
id: 'calendly_list_event_invitees',
|
||||
name: 'Calendly List Event Invitees',
|
||||
description: 'Retrieve a list of invitees for a scheduled event',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Calendly Personal Access Token',
|
||||
},
|
||||
eventUuid: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Scheduled event UUID (can be full URI or just the UUID)',
|
||||
},
|
||||
count: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Number of results per page (default: 20, max: 100)',
|
||||
},
|
||||
email: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Filter invitees by email address',
|
||||
},
|
||||
pageToken: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Page token for pagination',
|
||||
},
|
||||
sort: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Sort order for results (e.g., "created_at:asc", "created_at:desc")',
|
||||
},
|
||||
status: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Filter by status ("active" or "canceled")',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: CalendlyListEventInviteesParams) => {
|
||||
const uuid = params.eventUuid.includes('/')
|
||||
? params.eventUuid.split('/').pop()
|
||||
: params.eventUuid
|
||||
const url = `https://api.calendly.com/scheduled_events/${uuid}/invitees`
|
||||
const queryParams = []
|
||||
|
||||
if (params.count) {
|
||||
queryParams.push(`count=${Number(params.count)}`)
|
||||
}
|
||||
|
||||
if (params.email) {
|
||||
queryParams.push(`email=${encodeURIComponent(params.email)}`)
|
||||
}
|
||||
|
||||
if (params.pageToken) {
|
||||
queryParams.push(`page_token=${encodeURIComponent(params.pageToken)}`)
|
||||
}
|
||||
|
||||
if (params.sort) {
|
||||
queryParams.push(`sort=${encodeURIComponent(params.sort)}`)
|
||||
}
|
||||
|
||||
if (params.status) {
|
||||
queryParams.push(`status=${encodeURIComponent(params.status)}`)
|
||||
}
|
||||
|
||||
return queryParams.length > 0 ? `${url}?${queryParams.join('&')}` : url
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: data,
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
collection: {
|
||||
type: 'array',
|
||||
description: 'Array of invitee objects',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
uri: { type: 'string', description: 'Canonical reference to the invitee' },
|
||||
email: { type: 'string', description: 'Invitee email address' },
|
||||
name: { type: 'string', description: 'Invitee full name' },
|
||||
first_name: { type: 'string', description: 'Invitee first name' },
|
||||
last_name: { type: 'string', description: 'Invitee last name' },
|
||||
status: { type: 'string', description: 'Invitee status (active or canceled)' },
|
||||
questions_and_answers: {
|
||||
type: 'array',
|
||||
description: 'Responses to custom questions',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
question: { type: 'string', description: 'Question text' },
|
||||
answer: { type: 'string', description: 'Invitee answer' },
|
||||
position: { type: 'number', description: 'Question order' },
|
||||
},
|
||||
},
|
||||
},
|
||||
timezone: { type: 'string', description: 'Invitee timezone' },
|
||||
event: { type: 'string', description: 'URI of the scheduled event' },
|
||||
created_at: { type: 'string', description: 'ISO timestamp when invitee was created' },
|
||||
updated_at: { type: 'string', description: 'ISO timestamp when invitee was updated' },
|
||||
cancel_url: { type: 'string', description: 'URL to cancel the booking' },
|
||||
reschedule_url: { type: 'string', description: 'URL to reschedule the booking' },
|
||||
rescheduled: { type: 'boolean', description: 'Whether invitee rescheduled' },
|
||||
},
|
||||
},
|
||||
},
|
||||
pagination: {
|
||||
type: 'object',
|
||||
description: 'Pagination information',
|
||||
properties: {
|
||||
count: { type: 'number', description: 'Number of results in this page' },
|
||||
next_page: { type: 'string', description: 'URL to next page (if available)' },
|
||||
previous_page: { type: 'string', description: 'URL to previous page (if available)' },
|
||||
next_page_token: { type: 'string', description: 'Token for next page' },
|
||||
previous_page_token: { type: 'string', description: 'Token for previous page' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
147
apps/sim/tools/calendly/list_event_types.ts
Normal file
147
apps/sim/tools/calendly/list_event_types.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
import type {
|
||||
CalendlyListEventTypesParams,
|
||||
CalendlyListEventTypesResponse,
|
||||
} from '@/tools/calendly/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const listEventTypesTool: ToolConfig<
|
||||
CalendlyListEventTypesParams,
|
||||
CalendlyListEventTypesResponse
|
||||
> = {
|
||||
id: 'calendly_list_event_types',
|
||||
name: 'Calendly List Event Types',
|
||||
description: 'Retrieve a list of all event types for a user or organization',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Calendly Personal Access Token',
|
||||
},
|
||||
user: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Return only event types that belong to this user (URI format)',
|
||||
},
|
||||
organization: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Return only event types that belong to this organization (URI format)',
|
||||
},
|
||||
count: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Number of results per page (default: 20, max: 100)',
|
||||
},
|
||||
pageToken: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Page token for pagination',
|
||||
},
|
||||
sort: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Sort order for results (e.g., "name:asc", "name:desc")',
|
||||
},
|
||||
active: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'When true, show only active event types. When false or unchecked, show all event types (both active and inactive).',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: CalendlyListEventTypesParams) => {
|
||||
const url = 'https://api.calendly.com/event_types'
|
||||
const queryParams = []
|
||||
|
||||
if (params.user) {
|
||||
queryParams.push(`user=${encodeURIComponent(params.user)}`)
|
||||
}
|
||||
|
||||
if (params.organization) {
|
||||
queryParams.push(`organization=${encodeURIComponent(params.organization)}`)
|
||||
}
|
||||
|
||||
if (params.count) {
|
||||
queryParams.push(`count=${Number(params.count)}`)
|
||||
}
|
||||
|
||||
if (params.pageToken) {
|
||||
queryParams.push(`page_token=${encodeURIComponent(params.pageToken)}`)
|
||||
}
|
||||
|
||||
if (params.sort) {
|
||||
queryParams.push(`sort=${encodeURIComponent(params.sort)}`)
|
||||
}
|
||||
|
||||
if (params.active === true) {
|
||||
queryParams.push('active=true')
|
||||
}
|
||||
|
||||
return queryParams.length > 0 ? `${url}?${queryParams.join('&')}` : url
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: data,
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
collection: {
|
||||
type: 'array',
|
||||
description: 'Array of event type objects',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
uri: { type: 'string', description: 'Canonical reference to the event type' },
|
||||
name: { type: 'string', description: 'Event type name' },
|
||||
active: { type: 'boolean', description: 'Whether the event type is active' },
|
||||
booking_method: {
|
||||
type: 'string',
|
||||
description: 'Booking method (e.g., "round_robin_or_collect", "collective")',
|
||||
},
|
||||
color: { type: 'string', description: 'Hex color code' },
|
||||
created_at: { type: 'string', description: 'ISO timestamp of creation' },
|
||||
description_html: { type: 'string', description: 'HTML formatted description' },
|
||||
description_plain: { type: 'string', description: 'Plain text description' },
|
||||
duration: { type: 'number', description: 'Duration in minutes' },
|
||||
scheduling_url: { type: 'string', description: 'URL to scheduling page' },
|
||||
slug: { type: 'string', description: 'Unique identifier for URLs' },
|
||||
type: { type: 'string', description: 'Event type classification' },
|
||||
updated_at: { type: 'string', description: 'ISO timestamp of last update' },
|
||||
},
|
||||
},
|
||||
},
|
||||
pagination: {
|
||||
type: 'object',
|
||||
description: 'Pagination information',
|
||||
properties: {
|
||||
count: { type: 'number', description: 'Number of results in this page' },
|
||||
next_page: { type: 'string', description: 'URL to next page (if available)' },
|
||||
previous_page: { type: 'string', description: 'URL to previous page (if available)' },
|
||||
next_page_token: { type: 'string', description: 'Token for next page' },
|
||||
previous_page_token: { type: 'string', description: 'Token for previous page' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
200
apps/sim/tools/calendly/list_scheduled_events.ts
Normal file
200
apps/sim/tools/calendly/list_scheduled_events.ts
Normal file
@@ -0,0 +1,200 @@
|
||||
import type {
|
||||
CalendlyListScheduledEventsParams,
|
||||
CalendlyListScheduledEventsResponse,
|
||||
} from '@/tools/calendly/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const listScheduledEventsTool: ToolConfig<
|
||||
CalendlyListScheduledEventsParams,
|
||||
CalendlyListScheduledEventsResponse
|
||||
> = {
|
||||
id: 'calendly_list_scheduled_events',
|
||||
name: 'Calendly List Scheduled Events',
|
||||
description: 'Retrieve a list of scheduled events for a user or organization',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Calendly Personal Access Token',
|
||||
},
|
||||
user: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Return events that belong to this user (URI format). Either "user" or "organization" must be provided.',
|
||||
},
|
||||
organization: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Return events that belong to this organization (URI format). Either "user" or "organization" must be provided.',
|
||||
},
|
||||
invitee_email: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Return events where invitee has this email',
|
||||
},
|
||||
count: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Number of results per page (default: 20, max: 100)',
|
||||
},
|
||||
max_start_time: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Return events with start time before this time (ISO 8601 format)',
|
||||
},
|
||||
min_start_time: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Return events with start time after this time (ISO 8601 format)',
|
||||
},
|
||||
pageToken: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Page token for pagination',
|
||||
},
|
||||
sort: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Sort order for results (e.g., "start_time:asc", "start_time:desc")',
|
||||
},
|
||||
status: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Filter by status ("active" or "canceled")',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: CalendlyListScheduledEventsParams) => {
|
||||
const url = 'https://api.calendly.com/scheduled_events'
|
||||
const queryParams = []
|
||||
|
||||
if (!params.user && !params.organization) {
|
||||
throw new Error(
|
||||
'At least one of "user" or "organization" parameter is required. Please provide either a user URI or organization URI.'
|
||||
)
|
||||
}
|
||||
|
||||
if (params.user) {
|
||||
queryParams.push(`user=${encodeURIComponent(params.user)}`)
|
||||
}
|
||||
|
||||
if (params.organization) {
|
||||
queryParams.push(`organization=${encodeURIComponent(params.organization)}`)
|
||||
}
|
||||
|
||||
if (params.invitee_email) {
|
||||
queryParams.push(`invitee_email=${encodeURIComponent(params.invitee_email)}`)
|
||||
}
|
||||
|
||||
if (params.count) {
|
||||
queryParams.push(`count=${Number(params.count)}`)
|
||||
}
|
||||
|
||||
if (params.max_start_time) {
|
||||
queryParams.push(`max_start_time=${encodeURIComponent(params.max_start_time)}`)
|
||||
}
|
||||
|
||||
if (params.min_start_time) {
|
||||
queryParams.push(`min_start_time=${encodeURIComponent(params.min_start_time)}`)
|
||||
}
|
||||
|
||||
if (params.pageToken) {
|
||||
queryParams.push(`page_token=${encodeURIComponent(params.pageToken)}`)
|
||||
}
|
||||
|
||||
if (params.sort) {
|
||||
queryParams.push(`sort=${encodeURIComponent(params.sort)}`)
|
||||
}
|
||||
|
||||
if (params.status) {
|
||||
queryParams.push(`status=${encodeURIComponent(params.status)}`)
|
||||
}
|
||||
|
||||
return queryParams.length > 0 ? `${url}?${queryParams.join('&')}` : url
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: data,
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
collection: {
|
||||
type: 'array',
|
||||
description: 'Array of scheduled event objects',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
uri: { type: 'string', description: 'Canonical reference to the event' },
|
||||
name: { type: 'string', description: 'Event name' },
|
||||
status: { type: 'string', description: 'Event status (active or canceled)' },
|
||||
start_time: { type: 'string', description: 'ISO timestamp of event start' },
|
||||
end_time: { type: 'string', description: 'ISO timestamp of event end' },
|
||||
event_type: { type: 'string', description: 'URI of the event type' },
|
||||
location: {
|
||||
type: 'object',
|
||||
description: 'Event location details',
|
||||
properties: {
|
||||
type: {
|
||||
type: 'string',
|
||||
description: 'Location type (e.g., "zoom", "google_meet", "physical")',
|
||||
},
|
||||
location: { type: 'string', description: 'Location description' },
|
||||
join_url: {
|
||||
type: 'string',
|
||||
description: 'URL to join online meeting (if applicable)',
|
||||
},
|
||||
},
|
||||
},
|
||||
invitees_counter: {
|
||||
type: 'object',
|
||||
description: 'Invitee count information',
|
||||
properties: {
|
||||
total: { type: 'number', description: 'Total number of invitees' },
|
||||
active: { type: 'number', description: 'Number of active invitees' },
|
||||
limit: { type: 'number', description: 'Maximum number of invitees' },
|
||||
},
|
||||
},
|
||||
created_at: { type: 'string', description: 'ISO timestamp of event creation' },
|
||||
updated_at: { type: 'string', description: 'ISO timestamp of last update' },
|
||||
},
|
||||
},
|
||||
},
|
||||
pagination: {
|
||||
type: 'object',
|
||||
description: 'Pagination information',
|
||||
properties: {
|
||||
count: { type: 'number', description: 'Number of results in this page' },
|
||||
next_page: { type: 'string', description: 'URL to next page (if available)' },
|
||||
previous_page: { type: 'string', description: 'URL to previous page (if available)' },
|
||||
next_page_token: { type: 'string', description: 'Token for next page' },
|
||||
previous_page_token: { type: 'string', description: 'Token for previous page' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
133
apps/sim/tools/calendly/list_webhooks.ts
Normal file
133
apps/sim/tools/calendly/list_webhooks.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
import type {
|
||||
CalendlyListWebhooksParams,
|
||||
CalendlyListWebhooksResponse,
|
||||
} from '@/tools/calendly/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const listWebhooksTool: ToolConfig<
|
||||
CalendlyListWebhooksParams,
|
||||
CalendlyListWebhooksResponse
|
||||
> = {
|
||||
id: 'calendly_list_webhooks',
|
||||
name: 'Calendly List Webhooks',
|
||||
description: 'Retrieve a list of webhook subscriptions for an organization',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Calendly Personal Access Token',
|
||||
},
|
||||
organization: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Organization URI to list webhooks for',
|
||||
},
|
||||
count: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Number of results per page (default: 20, max: 100)',
|
||||
},
|
||||
pageToken: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Page token for pagination',
|
||||
},
|
||||
scope: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Filter by scope ("organization" or "user")',
|
||||
},
|
||||
user: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Filter webhooks by user URI (for user-scoped webhooks)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: CalendlyListWebhooksParams) => {
|
||||
const url = 'https://api.calendly.com/webhook_subscriptions'
|
||||
const queryParams = []
|
||||
|
||||
queryParams.push(`organization=${encodeURIComponent(params.organization)}`)
|
||||
|
||||
if (params.count) {
|
||||
queryParams.push(`count=${Number(params.count)}`)
|
||||
}
|
||||
|
||||
if (params.pageToken) {
|
||||
queryParams.push(`page_token=${encodeURIComponent(params.pageToken)}`)
|
||||
}
|
||||
|
||||
if (params.scope) {
|
||||
queryParams.push(`scope=${encodeURIComponent(params.scope)}`)
|
||||
}
|
||||
|
||||
if (params.user) {
|
||||
queryParams.push(`user=${encodeURIComponent(params.user)}`)
|
||||
}
|
||||
|
||||
return `${url}?${queryParams.join('&')}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: data,
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
collection: {
|
||||
type: 'array',
|
||||
description: 'Array of webhook subscription objects',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
uri: { type: 'string', description: 'Canonical reference to the webhook' },
|
||||
callback_url: { type: 'string', description: 'URL to receive webhook events' },
|
||||
created_at: { type: 'string', description: 'ISO timestamp of creation' },
|
||||
updated_at: { type: 'string', description: 'ISO timestamp of last update' },
|
||||
state: { type: 'string', description: 'Webhook state (active, disabled, etc.)' },
|
||||
events: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
description: 'Event types this webhook subscribes to',
|
||||
},
|
||||
signing_key: { type: 'string', description: 'Key to verify webhook signatures' },
|
||||
scope: { type: 'string', description: 'Webhook scope (organization or user)' },
|
||||
organization: { type: 'string', description: 'Organization URI' },
|
||||
user: { type: 'string', description: 'User URI (for user-scoped webhooks)' },
|
||||
creator: { type: 'string', description: 'URI of user who created the webhook' },
|
||||
},
|
||||
},
|
||||
},
|
||||
pagination: {
|
||||
type: 'object',
|
||||
description: 'Pagination information',
|
||||
properties: {
|
||||
count: { type: 'number', description: 'Number of results in this page' },
|
||||
next_page: { type: 'string', description: 'URL to next page (if available)' },
|
||||
previous_page: { type: 'string', description: 'URL to previous page (if available)' },
|
||||
next_page_token: { type: 'string', description: 'Token for next page' },
|
||||
previous_page_token: { type: 'string', description: 'Token for previous page' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
385
apps/sim/tools/calendly/types.ts
Normal file
385
apps/sim/tools/calendly/types.ts
Normal file
@@ -0,0 +1,385 @@
|
||||
import type { ToolResponse } from '@/tools/types'
|
||||
|
||||
export interface CalendlyGetCurrentUserParams {
|
||||
apiKey: string
|
||||
}
|
||||
|
||||
export interface CalendlyGetCurrentUserResponse extends ToolResponse {
|
||||
output: {
|
||||
resource: {
|
||||
uri: string
|
||||
name: string
|
||||
slug: string
|
||||
email: string
|
||||
scheduling_url: string
|
||||
timezone: string
|
||||
avatar_url: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
current_organization: string
|
||||
resource_type: string
|
||||
locale: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface CalendlyListEventTypesParams {
|
||||
apiKey: string
|
||||
user?: string
|
||||
organization?: string
|
||||
count?: number
|
||||
pageToken?: string
|
||||
sort?: string
|
||||
active?: boolean
|
||||
}
|
||||
|
||||
export interface CalendlyListEventTypesResponse extends ToolResponse {
|
||||
output: {
|
||||
collection: Array<{
|
||||
uri: string
|
||||
name: string
|
||||
active: boolean
|
||||
booking_method: string
|
||||
color: string
|
||||
created_at: string
|
||||
description_html: string
|
||||
description_plain: string
|
||||
duration: number
|
||||
internal_note: string
|
||||
kind: string
|
||||
pooling_type: string
|
||||
profile: {
|
||||
name: string
|
||||
owner: string
|
||||
type: string
|
||||
}
|
||||
scheduling_url: string
|
||||
slug: string
|
||||
type: string
|
||||
updated_at: string
|
||||
}>
|
||||
pagination: {
|
||||
count: number
|
||||
next_page: string | null
|
||||
previous_page: string | null
|
||||
next_page_token: string | null
|
||||
previous_page_token: string | null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface CalendlyGetEventTypeParams {
|
||||
apiKey: string
|
||||
eventTypeUuid: string
|
||||
}
|
||||
|
||||
export interface CalendlyGetEventTypeResponse extends ToolResponse {
|
||||
output: {
|
||||
resource: {
|
||||
uri: string
|
||||
name: string
|
||||
active: boolean
|
||||
booking_method: string
|
||||
color: string
|
||||
created_at: string
|
||||
custom_questions: Array<{
|
||||
name: string
|
||||
type: string
|
||||
position: number
|
||||
enabled: boolean
|
||||
required: boolean
|
||||
answer_choices: string[]
|
||||
include_other: boolean
|
||||
}>
|
||||
deleted_at: string | null
|
||||
description_html: string
|
||||
description_plain: string
|
||||
duration: number
|
||||
internal_note: string
|
||||
kind: string
|
||||
pooling_type: string
|
||||
profile: {
|
||||
name: string
|
||||
owner: string
|
||||
type: string
|
||||
}
|
||||
scheduling_url: string
|
||||
slug: string
|
||||
type: string
|
||||
updated_at: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface CalendlyListScheduledEventsParams {
|
||||
apiKey: string
|
||||
user?: string
|
||||
organization?: string
|
||||
invitee_email?: string
|
||||
count?: number
|
||||
max_start_time?: string
|
||||
min_start_time?: string
|
||||
pageToken?: string
|
||||
sort?: string
|
||||
status?: string
|
||||
}
|
||||
|
||||
export interface CalendlyListScheduledEventsResponse extends ToolResponse {
|
||||
output: {
|
||||
collection: Array<{
|
||||
uri: string
|
||||
name: string
|
||||
status: string
|
||||
start_time: string
|
||||
end_time: string
|
||||
event_type: string
|
||||
location: {
|
||||
type: string
|
||||
location: string
|
||||
join_url?: string
|
||||
data?: Record<string, any>
|
||||
}
|
||||
invitees_counter: {
|
||||
total: number
|
||||
active: number
|
||||
limit: number
|
||||
}
|
||||
created_at: string
|
||||
updated_at: string
|
||||
event_memberships: Array<{
|
||||
user: string
|
||||
user_email: string
|
||||
user_name: string
|
||||
}>
|
||||
event_guests: Array<{
|
||||
email: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}>
|
||||
cancellation?: {
|
||||
canceled_by: string
|
||||
reason: string
|
||||
canceler_type: string
|
||||
}
|
||||
}>
|
||||
pagination: {
|
||||
count: number
|
||||
next_page: string | null
|
||||
previous_page: string | null
|
||||
next_page_token: string | null
|
||||
previous_page_token: string | null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface CalendlyGetScheduledEventParams {
|
||||
apiKey: string
|
||||
eventUuid: string
|
||||
}
|
||||
|
||||
export interface CalendlyGetScheduledEventResponse extends ToolResponse {
|
||||
output: {
|
||||
resource: {
|
||||
uri: string
|
||||
name: string
|
||||
status: string
|
||||
start_time: string
|
||||
end_time: string
|
||||
event_type: string
|
||||
location: {
|
||||
type: string
|
||||
location: string
|
||||
join_url?: string
|
||||
data?: Record<string, any>
|
||||
}
|
||||
invitees_counter: {
|
||||
total: number
|
||||
active: number
|
||||
limit: number
|
||||
}
|
||||
created_at: string
|
||||
updated_at: string
|
||||
event_memberships: Array<{
|
||||
user: string
|
||||
user_email: string
|
||||
user_name: string
|
||||
}>
|
||||
event_guests: Array<{
|
||||
email: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}>
|
||||
cancellation?: {
|
||||
canceled_by: string
|
||||
reason: string
|
||||
canceler_type: string
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface CalendlyListEventInviteesParams {
|
||||
apiKey: string
|
||||
eventUuid: string
|
||||
count?: number
|
||||
email?: string
|
||||
pageToken?: string
|
||||
sort?: string
|
||||
status?: string
|
||||
}
|
||||
|
||||
export interface CalendlyListEventInviteesResponse extends ToolResponse {
|
||||
output: {
|
||||
collection: Array<{
|
||||
uri: string
|
||||
email: string
|
||||
name: string
|
||||
first_name: string | null
|
||||
last_name: string | null
|
||||
status: string
|
||||
questions_and_answers: Array<{
|
||||
question: string
|
||||
answer: string
|
||||
position: number
|
||||
}>
|
||||
timezone: string
|
||||
event: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
tracking: {
|
||||
utm_campaign: string | null
|
||||
utm_source: string | null
|
||||
utm_medium: string | null
|
||||
utm_content: string | null
|
||||
utm_term: string | null
|
||||
salesforce_uuid: string | null
|
||||
}
|
||||
text_reminder_number: string | null
|
||||
rescheduled: boolean
|
||||
old_invitee: string | null
|
||||
new_invitee: string | null
|
||||
cancel_url: string
|
||||
reschedule_url: string
|
||||
cancellation?: {
|
||||
canceled_by: string
|
||||
reason: string
|
||||
canceler_type: string
|
||||
}
|
||||
payment?: {
|
||||
id: string
|
||||
provider: string
|
||||
amount: number
|
||||
currency: string
|
||||
terms: string
|
||||
successful: boolean
|
||||
}
|
||||
no_show?: {
|
||||
created_at: string
|
||||
}
|
||||
reconfirmation?: {
|
||||
created_at: string
|
||||
confirmed_at: string | null
|
||||
}
|
||||
}>
|
||||
pagination: {
|
||||
count: number
|
||||
next_page: string | null
|
||||
previous_page: string | null
|
||||
next_page_token: string | null
|
||||
previous_page_token: string | null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface CalendlyCancelEventParams {
|
||||
apiKey: string
|
||||
eventUuid: string
|
||||
reason?: string
|
||||
}
|
||||
|
||||
export interface CalendlyCancelEventResponse extends ToolResponse {
|
||||
output: {
|
||||
resource: {
|
||||
canceler_type: string
|
||||
canceled_by: string
|
||||
reason: string | null
|
||||
created_at: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface CalendlyListWebhooksParams {
|
||||
apiKey: string
|
||||
organization: string
|
||||
count?: number
|
||||
pageToken?: string
|
||||
scope?: string
|
||||
user?: string
|
||||
}
|
||||
|
||||
export interface CalendlyListWebhooksResponse extends ToolResponse {
|
||||
output: {
|
||||
collection: Array<{
|
||||
uri: string
|
||||
callback_url: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
retry_started_at: string | null
|
||||
state: string
|
||||
events: string[]
|
||||
signing_key: string
|
||||
scope: string
|
||||
organization: string
|
||||
user?: string
|
||||
creator: string
|
||||
}>
|
||||
pagination: {
|
||||
count: number
|
||||
next_page: string | null
|
||||
previous_page: string | null
|
||||
next_page_token: string | null
|
||||
previous_page_token: string | null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface CalendlyCreateWebhookParams {
|
||||
apiKey: string
|
||||
url: string
|
||||
events: string[]
|
||||
organization: string
|
||||
user?: string
|
||||
scope: string
|
||||
signing_key?: string
|
||||
}
|
||||
|
||||
export interface CalendlyCreateWebhookResponse extends ToolResponse {
|
||||
output: {
|
||||
resource: {
|
||||
uri: string
|
||||
callback_url: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
retry_started_at: string | null
|
||||
state: string
|
||||
events: string[]
|
||||
signing_key: string
|
||||
scope: string
|
||||
organization: string
|
||||
user?: string
|
||||
creator: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface CalendlyDeleteWebhookParams {
|
||||
apiKey: string
|
||||
webhookUuid: string
|
||||
}
|
||||
|
||||
export interface CalendlyDeleteWebhookResponse extends ToolResponse {
|
||||
output: {
|
||||
deleted: boolean
|
||||
message: string
|
||||
}
|
||||
}
|
||||
@@ -11,10 +11,16 @@ export const memoryAddTool: ToolConfig<any, MemoryResponse> = {
|
||||
params: {
|
||||
conversationId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
required: false,
|
||||
description:
|
||||
'Conversation identifier (e.g., user-123, session-abc). If a memory with this conversationId already exists for this block, the new message will be appended to it.',
|
||||
},
|
||||
id: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
description:
|
||||
'Legacy parameter for conversation identifier. Use conversationId instead. Provided for backwards compatibility.',
|
||||
},
|
||||
role: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
@@ -29,7 +35,7 @@ export const memoryAddTool: ToolConfig<any, MemoryResponse> = {
|
||||
type: 'string',
|
||||
required: false,
|
||||
description:
|
||||
'Optional block ID. If not provided, uses the current block ID from execution context.',
|
||||
'Optional block ID. If not provided, uses the current block ID from execution context, or defaults to "default".',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -57,30 +63,20 @@ export const memoryAddTool: ToolConfig<any, MemoryResponse> = {
|
||||
}
|
||||
}
|
||||
|
||||
const blockId = params.blockId || contextBlockId
|
||||
if (!blockId) {
|
||||
return {
|
||||
_errorResponse: {
|
||||
status: 400,
|
||||
data: {
|
||||
success: false,
|
||||
error: {
|
||||
message:
|
||||
'blockId is required. Either provide it as a parameter or ensure it is available in execution context.',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
// Use 'id' as fallback for 'conversationId' for backwards compatibility
|
||||
const conversationId = params.conversationId || params.id
|
||||
|
||||
if (!params.conversationId || params.conversationId.trim() === '') {
|
||||
// Default blockId to 'default' if not provided in params or context
|
||||
const blockId = params.blockId || contextBlockId || 'default'
|
||||
|
||||
if (!conversationId || conversationId.trim() === '') {
|
||||
return {
|
||||
_errorResponse: {
|
||||
status: 400,
|
||||
data: {
|
||||
success: false,
|
||||
error: {
|
||||
message: 'conversationId is required',
|
||||
message: 'conversationId or id is required',
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -101,7 +97,7 @@ export const memoryAddTool: ToolConfig<any, MemoryResponse> = {
|
||||
}
|
||||
}
|
||||
|
||||
const key = buildMemoryKey(params.conversationId, blockId)
|
||||
const key = buildMemoryKey(conversationId, blockId)
|
||||
|
||||
const body: Record<string, any> = {
|
||||
key,
|
||||
|
||||
@@ -15,6 +15,12 @@ export const memoryDeleteTool: ToolConfig<any, MemoryResponse> = {
|
||||
description:
|
||||
'Conversation identifier (e.g., user-123, session-abc). If provided alone, deletes all memories for this conversation across all blocks.',
|
||||
},
|
||||
id: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
description:
|
||||
'Legacy parameter for conversation identifier. Use conversationId instead. Provided for backwards compatibility.',
|
||||
},
|
||||
blockId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
@@ -47,14 +53,18 @@ export const memoryDeleteTool: ToolConfig<any, MemoryResponse> = {
|
||||
}
|
||||
}
|
||||
|
||||
if (!params.conversationId && !params.blockId && !params.blockName) {
|
||||
// Use 'id' as fallback for 'conversationId' for backwards compatibility
|
||||
const conversationId = params.conversationId || params.id
|
||||
|
||||
if (!conversationId && !params.blockId && !params.blockName) {
|
||||
return {
|
||||
_errorResponse: {
|
||||
status: 400,
|
||||
data: {
|
||||
success: false,
|
||||
error: {
|
||||
message: 'At least one of conversationId, blockId, or blockName must be provided',
|
||||
message:
|
||||
'At least one of conversationId, id, blockId, or blockName must be provided',
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -64,8 +74,8 @@ export const memoryDeleteTool: ToolConfig<any, MemoryResponse> = {
|
||||
const url = new URL('/api/memory', 'http://dummy')
|
||||
url.searchParams.set('workflowId', workflowId)
|
||||
|
||||
if (params.conversationId) {
|
||||
url.searchParams.set('conversationId', params.conversationId)
|
||||
if (conversationId) {
|
||||
url.searchParams.set('conversationId', conversationId)
|
||||
}
|
||||
if (params.blockId) {
|
||||
url.searchParams.set('blockId', params.blockId)
|
||||
|
||||
@@ -16,6 +16,12 @@ export const memoryGetTool: ToolConfig<any, MemoryResponse> = {
|
||||
description:
|
||||
'Conversation identifier (e.g., user-123, session-abc). If provided alone, returns all memories for this conversation across all blocks.',
|
||||
},
|
||||
id: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
description:
|
||||
'Legacy parameter for conversation identifier. Use conversationId instead. Provided for backwards compatibility.',
|
||||
},
|
||||
blockId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
@@ -48,14 +54,18 @@ export const memoryGetTool: ToolConfig<any, MemoryResponse> = {
|
||||
}
|
||||
}
|
||||
|
||||
if (!params.conversationId && !params.blockId && !params.blockName) {
|
||||
// Use 'id' as fallback for 'conversationId' for backwards compatibility
|
||||
const conversationId = params.conversationId || params.id
|
||||
|
||||
if (!conversationId && !params.blockId && !params.blockName) {
|
||||
return {
|
||||
_errorResponse: {
|
||||
status: 400,
|
||||
data: {
|
||||
success: false,
|
||||
error: {
|
||||
message: 'At least one of conversationId, blockId, or blockName must be provided',
|
||||
message:
|
||||
'At least one of conversationId, id, blockId, or blockName must be provided',
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -64,10 +74,11 @@ export const memoryGetTool: ToolConfig<any, MemoryResponse> = {
|
||||
|
||||
let query = ''
|
||||
|
||||
if (params.conversationId && params.blockId) {
|
||||
query = buildMemoryKey(params.conversationId, params.blockId)
|
||||
} else if (params.conversationId) {
|
||||
query = `${params.conversationId}:`
|
||||
if (conversationId && params.blockId) {
|
||||
query = buildMemoryKey(conversationId, params.blockId)
|
||||
} else if (conversationId) {
|
||||
// Also check for legacy format (conversationId without blockId)
|
||||
query = `${conversationId}:`
|
||||
} else if (params.blockId) {
|
||||
query = `:${params.blockId}`
|
||||
}
|
||||
|
||||
@@ -1,18 +1,36 @@
|
||||
/**
|
||||
* Parse memory key into conversationId and blockId
|
||||
* Key format: conversationId:blockId
|
||||
* Supports two formats:
|
||||
* - New format: conversationId:blockId (splits on LAST colon to handle IDs with colons)
|
||||
* - Legacy format: id (without colon, treated as conversationId with blockId='default')
|
||||
* @param key The memory key to parse
|
||||
* @returns Object with conversationId and blockId, or null if invalid
|
||||
*/
|
||||
export function parseMemoryKey(key: string): { conversationId: string; blockId: string } | null {
|
||||
const parts = key.split(':')
|
||||
if (parts.length !== 2) {
|
||||
if (!key) {
|
||||
return null
|
||||
}
|
||||
|
||||
const lastColonIndex = key.lastIndexOf(':')
|
||||
|
||||
// Legacy format: no colon found
|
||||
if (lastColonIndex === -1) {
|
||||
return {
|
||||
conversationId: parts[0],
|
||||
blockId: parts[1],
|
||||
conversationId: key,
|
||||
blockId: 'default',
|
||||
}
|
||||
}
|
||||
|
||||
// Invalid: colon at start or end
|
||||
if (lastColonIndex === 0 || lastColonIndex === key.length - 1) {
|
||||
return null
|
||||
}
|
||||
|
||||
// New format: split on last colon to handle IDs with colons
|
||||
// This allows conversationIds like "user:123" to work correctly
|
||||
return {
|
||||
conversationId: key.substring(0, lastColonIndex),
|
||||
blockId: key.substring(lastColonIndex + 1),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
104
apps/sim/tools/neo4j/create.ts
Normal file
104
apps/sim/tools/neo4j/create.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import type { Neo4jCreateParams, Neo4jResponse } from '@/tools/neo4j/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const createTool: ToolConfig<Neo4jCreateParams, Neo4jResponse> = {
|
||||
id: 'neo4j_create',
|
||||
name: 'Neo4j Create',
|
||||
description:
|
||||
'Execute CREATE statements to add new nodes and relationships to Neo4j graph database',
|
||||
version: '1.0',
|
||||
|
||||
params: {
|
||||
host: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Neo4j server hostname or IP address',
|
||||
},
|
||||
port: {
|
||||
type: 'number',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Neo4j server port (default: 7687 for Bolt protocol)',
|
||||
},
|
||||
database: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Database name to connect to',
|
||||
},
|
||||
username: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Neo4j username',
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Neo4j password',
|
||||
},
|
||||
encryption: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Connection encryption mode (enabled, disabled)',
|
||||
},
|
||||
cypherQuery: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Cypher CREATE statement to execute',
|
||||
},
|
||||
parameters: {
|
||||
type: 'object',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Parameters for the Cypher query as a JSON object',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: '/api/tools/neo4j/create',
|
||||
method: 'POST',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => ({
|
||||
host: params.host,
|
||||
port: Number(params.port),
|
||||
database: params.database,
|
||||
username: params.username,
|
||||
password: params.password,
|
||||
encryption: params.encryption || 'disabled',
|
||||
cypherQuery: params.cypherQuery,
|
||||
parameters: params.parameters,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || 'Neo4j create operation failed')
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
message: data.message || 'Create operation executed successfully',
|
||||
summary: data.summary,
|
||||
},
|
||||
error: undefined,
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
message: { type: 'string', description: 'Operation status message' },
|
||||
summary: {
|
||||
type: 'json',
|
||||
description: 'Creation summary with counters for nodes and relationships created',
|
||||
},
|
||||
},
|
||||
}
|
||||
111
apps/sim/tools/neo4j/delete.ts
Normal file
111
apps/sim/tools/neo4j/delete.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import type { Neo4jDeleteParams, Neo4jResponse } from '@/tools/neo4j/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const deleteTool: ToolConfig<Neo4jDeleteParams, Neo4jResponse> = {
|
||||
id: 'neo4j_delete',
|
||||
name: 'Neo4j Delete',
|
||||
description:
|
||||
'Execute DELETE or DETACH DELETE statements to remove nodes and relationships from Neo4j',
|
||||
version: '1.0',
|
||||
|
||||
params: {
|
||||
host: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Neo4j server hostname or IP address',
|
||||
},
|
||||
port: {
|
||||
type: 'number',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Neo4j server port (default: 7687 for Bolt protocol)',
|
||||
},
|
||||
database: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Database name to connect to',
|
||||
},
|
||||
username: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Neo4j username',
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Neo4j password',
|
||||
},
|
||||
encryption: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Connection encryption mode (enabled, disabled)',
|
||||
},
|
||||
cypherQuery: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Cypher query with MATCH and DELETE/DETACH DELETE statements',
|
||||
},
|
||||
parameters: {
|
||||
type: 'object',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Parameters for the Cypher query as a JSON object',
|
||||
},
|
||||
detach: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Whether to use DETACH DELETE to remove relationships before deleting nodes',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: '/api/tools/neo4j/delete',
|
||||
method: 'POST',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => ({
|
||||
host: params.host,
|
||||
port: Number(params.port),
|
||||
database: params.database,
|
||||
username: params.username,
|
||||
password: params.password,
|
||||
encryption: params.encryption || 'disabled',
|
||||
cypherQuery: params.cypherQuery,
|
||||
parameters: params.parameters,
|
||||
detach: params.detach,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || 'Neo4j delete operation failed')
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
message: data.message || 'Delete operation executed successfully',
|
||||
summary: data.summary,
|
||||
},
|
||||
error: undefined,
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
message: { type: 'string', description: 'Operation status message' },
|
||||
summary: {
|
||||
type: 'json',
|
||||
description: 'Delete summary with counters for nodes and relationships deleted',
|
||||
},
|
||||
},
|
||||
}
|
||||
104
apps/sim/tools/neo4j/execute.ts
Normal file
104
apps/sim/tools/neo4j/execute.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import type { Neo4jExecuteParams, Neo4jResponse } from '@/tools/neo4j/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const executeTool: ToolConfig<Neo4jExecuteParams, Neo4jResponse> = {
|
||||
id: 'neo4j_execute',
|
||||
name: 'Neo4j Execute',
|
||||
description: 'Execute arbitrary Cypher queries on Neo4j graph database for complex operations',
|
||||
version: '1.0',
|
||||
|
||||
params: {
|
||||
host: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Neo4j server hostname or IP address',
|
||||
},
|
||||
port: {
|
||||
type: 'number',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Neo4j server port (default: 7687 for Bolt protocol)',
|
||||
},
|
||||
database: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Database name to connect to',
|
||||
},
|
||||
username: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Neo4j username',
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Neo4j password',
|
||||
},
|
||||
encryption: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Connection encryption mode (enabled, disabled)',
|
||||
},
|
||||
cypherQuery: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Cypher query to execute (any valid Cypher statement)',
|
||||
},
|
||||
parameters: {
|
||||
type: 'object',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Parameters for the Cypher query as a JSON object',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: '/api/tools/neo4j/execute',
|
||||
method: 'POST',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => ({
|
||||
host: params.host,
|
||||
port: Number(params.port),
|
||||
database: params.database,
|
||||
username: params.username,
|
||||
password: params.password,
|
||||
encryption: params.encryption || 'disabled',
|
||||
cypherQuery: params.cypherQuery,
|
||||
parameters: params.parameters,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || 'Neo4j execute operation failed')
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
message: data.message || 'Query executed successfully',
|
||||
records: data.records || [],
|
||||
recordCount: data.recordCount || 0,
|
||||
summary: data.summary,
|
||||
},
|
||||
error: undefined,
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
message: { type: 'string', description: 'Operation status message' },
|
||||
records: { type: 'array', description: 'Array of records returned from the query' },
|
||||
recordCount: { type: 'number', description: 'Number of records returned' },
|
||||
summary: { type: 'json', description: 'Execution summary with timing and counters' },
|
||||
},
|
||||
}
|
||||
7
apps/sim/tools/neo4j/index.ts
Normal file
7
apps/sim/tools/neo4j/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export { createTool } from './create'
|
||||
export { deleteTool } from './delete'
|
||||
export { executeTool } from './execute'
|
||||
export { mergeTool } from './merge'
|
||||
export { queryTool } from './query'
|
||||
export * from './types'
|
||||
export { updateTool } from './update'
|
||||
104
apps/sim/tools/neo4j/merge.ts
Normal file
104
apps/sim/tools/neo4j/merge.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import type { Neo4jMergeParams, Neo4jResponse } from '@/tools/neo4j/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const mergeTool: ToolConfig<Neo4jMergeParams, Neo4jResponse> = {
|
||||
id: 'neo4j_merge',
|
||||
name: 'Neo4j Merge',
|
||||
description:
|
||||
'Execute MERGE statements to find or create nodes and relationships in Neo4j (upsert operation)',
|
||||
version: '1.0',
|
||||
|
||||
params: {
|
||||
host: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Neo4j server hostname or IP address',
|
||||
},
|
||||
port: {
|
||||
type: 'number',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Neo4j server port (default: 7687 for Bolt protocol)',
|
||||
},
|
||||
database: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Database name to connect to',
|
||||
},
|
||||
username: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Neo4j username',
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Neo4j password',
|
||||
},
|
||||
encryption: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Connection encryption mode (enabled, disabled)',
|
||||
},
|
||||
cypherQuery: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Cypher MERGE statement to execute',
|
||||
},
|
||||
parameters: {
|
||||
type: 'object',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Parameters for the Cypher query as a JSON object',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: '/api/tools/neo4j/merge',
|
||||
method: 'POST',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => ({
|
||||
host: params.host,
|
||||
port: Number(params.port),
|
||||
database: params.database,
|
||||
username: params.username,
|
||||
password: params.password,
|
||||
encryption: params.encryption || 'disabled',
|
||||
cypherQuery: params.cypherQuery,
|
||||
parameters: params.parameters,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || 'Neo4j merge operation failed')
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
message: data.message || 'Merge operation executed successfully',
|
||||
summary: data.summary,
|
||||
},
|
||||
error: undefined,
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
message: { type: 'string', description: 'Operation status message' },
|
||||
summary: {
|
||||
type: 'json',
|
||||
description: 'Merge summary with counters for nodes/relationships created or matched',
|
||||
},
|
||||
},
|
||||
}
|
||||
106
apps/sim/tools/neo4j/query.ts
Normal file
106
apps/sim/tools/neo4j/query.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import type { Neo4jQueryParams, Neo4jResponse } from '@/tools/neo4j/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const queryTool: ToolConfig<Neo4jQueryParams, Neo4jResponse> = {
|
||||
id: 'neo4j_query',
|
||||
name: 'Neo4j Query',
|
||||
description:
|
||||
'Execute MATCH queries to read nodes and relationships from Neo4j graph database. For best performance and to prevent large result sets, include LIMIT in your query (e.g., "MATCH (n:User) RETURN n LIMIT 100") or use LIMIT $limit with a limit parameter.',
|
||||
version: '1.0',
|
||||
|
||||
params: {
|
||||
host: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Neo4j server hostname or IP address',
|
||||
},
|
||||
port: {
|
||||
type: 'number',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Neo4j server port (default: 7687 for Bolt protocol)',
|
||||
},
|
||||
database: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Database name to connect to',
|
||||
},
|
||||
username: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Neo4j username',
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Neo4j password',
|
||||
},
|
||||
encryption: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Connection encryption mode (enabled, disabled)',
|
||||
},
|
||||
cypherQuery: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Cypher query to execute (typically MATCH statements)',
|
||||
},
|
||||
parameters: {
|
||||
type: 'object',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Parameters for the Cypher query as a JSON object. Use for any dynamic values including LIMIT (e.g., query: "MATCH (n) RETURN n LIMIT $limit", parameters: {limit: 100}).',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: '/api/tools/neo4j/query',
|
||||
method: 'POST',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => ({
|
||||
host: params.host,
|
||||
port: Number(params.port),
|
||||
database: params.database,
|
||||
username: params.username,
|
||||
password: params.password,
|
||||
encryption: params.encryption || 'disabled',
|
||||
cypherQuery: params.cypherQuery,
|
||||
parameters: params.parameters,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || 'Neo4j query failed')
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
message: data.message || 'Query executed successfully',
|
||||
records: data.records || [],
|
||||
recordCount: data.recordCount || 0,
|
||||
summary: data.summary,
|
||||
},
|
||||
error: undefined,
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
message: { type: 'string', description: 'Operation status message' },
|
||||
records: { type: 'array', description: 'Array of records returned from the query' },
|
||||
recordCount: { type: 'number', description: 'Number of records returned' },
|
||||
summary: { type: 'json', description: 'Query execution summary with timing and counters' },
|
||||
},
|
||||
}
|
||||
75
apps/sim/tools/neo4j/types.ts
Normal file
75
apps/sim/tools/neo4j/types.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import type { ToolResponse } from '@/tools/types'
|
||||
|
||||
export interface Neo4jConnectionConfig {
|
||||
host: string
|
||||
port: number
|
||||
database: string
|
||||
username: string
|
||||
password: string
|
||||
encryption?: 'enabled' | 'disabled'
|
||||
}
|
||||
|
||||
export interface Neo4jQueryParams extends Neo4jConnectionConfig {
|
||||
cypherQuery: string
|
||||
parameters?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export interface Neo4jCreateParams extends Neo4jConnectionConfig {
|
||||
cypherQuery: string
|
||||
parameters?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export interface Neo4jMergeParams extends Neo4jConnectionConfig {
|
||||
cypherQuery: string
|
||||
parameters?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export interface Neo4jUpdateParams extends Neo4jConnectionConfig {
|
||||
cypherQuery: string
|
||||
parameters?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export interface Neo4jDeleteParams extends Neo4jConnectionConfig {
|
||||
cypherQuery: string
|
||||
parameters?: Record<string, unknown>
|
||||
detach?: boolean
|
||||
}
|
||||
|
||||
export interface Neo4jExecuteParams extends Neo4jConnectionConfig {
|
||||
cypherQuery: string
|
||||
parameters?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export interface Neo4jBaseResponse extends ToolResponse {
|
||||
output: {
|
||||
message: string
|
||||
records?: unknown[]
|
||||
recordCount?: number
|
||||
summary?: {
|
||||
resultAvailableAfter: number
|
||||
resultConsumedAfter: number
|
||||
counters?: {
|
||||
nodesCreated: number
|
||||
nodesDeleted: number
|
||||
relationshipsCreated: number
|
||||
relationshipsDeleted: number
|
||||
propertiesSet: number
|
||||
labelsAdded: number
|
||||
labelsRemoved: number
|
||||
indexesAdded: number
|
||||
indexesRemoved: number
|
||||
constraintsAdded: number
|
||||
constraintsRemoved: number
|
||||
}
|
||||
}
|
||||
}
|
||||
error?: string
|
||||
}
|
||||
|
||||
export interface Neo4jQueryResponse extends Neo4jBaseResponse {}
|
||||
export interface Neo4jCreateResponse extends Neo4jBaseResponse {}
|
||||
export interface Neo4jMergeResponse extends Neo4jBaseResponse {}
|
||||
export interface Neo4jUpdateResponse extends Neo4jBaseResponse {}
|
||||
export interface Neo4jDeleteResponse extends Neo4jBaseResponse {}
|
||||
export interface Neo4jExecuteResponse extends Neo4jBaseResponse {}
|
||||
export interface Neo4jResponse extends Neo4jBaseResponse {}
|
||||
101
apps/sim/tools/neo4j/update.ts
Normal file
101
apps/sim/tools/neo4j/update.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import type { Neo4jResponse, Neo4jUpdateParams } from '@/tools/neo4j/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const updateTool: ToolConfig<Neo4jUpdateParams, Neo4jResponse> = {
|
||||
id: 'neo4j_update',
|
||||
name: 'Neo4j Update',
|
||||
description:
|
||||
'Execute SET statements to update properties of existing nodes and relationships in Neo4j',
|
||||
version: '1.0',
|
||||
|
||||
params: {
|
||||
host: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Neo4j server hostname or IP address',
|
||||
},
|
||||
port: {
|
||||
type: 'number',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Neo4j server port (default: 7687 for Bolt protocol)',
|
||||
},
|
||||
database: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Database name to connect to',
|
||||
},
|
||||
username: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Neo4j username',
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Neo4j password',
|
||||
},
|
||||
encryption: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Connection encryption mode (enabled, disabled)',
|
||||
},
|
||||
cypherQuery: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Cypher query with MATCH and SET statements to update properties',
|
||||
},
|
||||
parameters: {
|
||||
type: 'object',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Parameters for the Cypher query as a JSON object',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: '/api/tools/neo4j/update',
|
||||
method: 'POST',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => ({
|
||||
host: params.host,
|
||||
port: Number(params.port),
|
||||
database: params.database,
|
||||
username: params.username,
|
||||
password: params.password,
|
||||
encryption: params.encryption || 'disabled',
|
||||
cypherQuery: params.cypherQuery,
|
||||
parameters: params.parameters,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || 'Neo4j update operation failed')
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
message: data.message || 'Update operation executed successfully',
|
||||
summary: data.summary,
|
||||
},
|
||||
error: undefined,
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
message: { type: 'string', description: 'Operation status message' },
|
||||
summary: { type: 'json', description: 'Update summary with counters for properties set' },
|
||||
},
|
||||
}
|
||||
@@ -41,6 +41,18 @@ import {
|
||||
asanaUpdateTaskTool,
|
||||
} from '@/tools/asana'
|
||||
import { browserUseRunTaskTool } from '@/tools/browser_use'
|
||||
import {
|
||||
calendlyCancelEventTool,
|
||||
calendlyCreateWebhookTool,
|
||||
calendlyDeleteWebhookTool,
|
||||
calendlyGetCurrentUserTool,
|
||||
calendlyGetEventTypeTool,
|
||||
calendlyGetScheduledEventTool,
|
||||
calendlyListEventInviteesTool,
|
||||
calendlyListEventTypesTool,
|
||||
calendlyListScheduledEventsTool,
|
||||
calendlyListWebhooksTool,
|
||||
} from '@/tools/calendly'
|
||||
import { clayPopulateTool } from '@/tools/clay'
|
||||
import {
|
||||
confluenceCreateCommentTool,
|
||||
@@ -397,6 +409,14 @@ import {
|
||||
queryTool as mysqlQueryTool,
|
||||
updateTool as mysqlUpdateTool,
|
||||
} from '@/tools/mysql'
|
||||
import {
|
||||
createTool as neo4jCreateTool,
|
||||
deleteTool as neo4jDeleteTool,
|
||||
executeTool as neo4jExecuteTool,
|
||||
mergeTool as neo4jMergeTool,
|
||||
queryTool as neo4jQueryTool,
|
||||
updateTool as neo4jUpdateTool,
|
||||
} from '@/tools/neo4j'
|
||||
import {
|
||||
notionCreateDatabaseTool,
|
||||
notionCreatePageTool,
|
||||
@@ -774,6 +794,16 @@ export const tools: Record<string, ToolConfig> = {
|
||||
supabase_storage_delete_bucket: supabaseStorageDeleteBucketTool,
|
||||
supabase_storage_get_public_url: supabaseStorageGetPublicUrlTool,
|
||||
supabase_storage_create_signed_url: supabaseStorageCreateSignedUrlTool,
|
||||
calendly_get_current_user: calendlyGetCurrentUserTool,
|
||||
calendly_list_event_types: calendlyListEventTypesTool,
|
||||
calendly_get_event_type: calendlyGetEventTypeTool,
|
||||
calendly_list_scheduled_events: calendlyListScheduledEventsTool,
|
||||
calendly_get_scheduled_event: calendlyGetScheduledEventTool,
|
||||
calendly_list_event_invitees: calendlyListEventInviteesTool,
|
||||
calendly_cancel_event: calendlyCancelEventTool,
|
||||
calendly_list_webhooks: calendlyListWebhooksTool,
|
||||
calendly_create_webhook: calendlyCreateWebhookTool,
|
||||
calendly_delete_webhook: calendlyDeleteWebhookTool,
|
||||
typeform_responses: typeformResponsesTool,
|
||||
typeform_files: typeformFilesTool,
|
||||
typeform_insights: typeformInsightsTool,
|
||||
@@ -852,6 +882,12 @@ export const tools: Record<string, ToolConfig> = {
|
||||
mysql_update: mysqlUpdateTool,
|
||||
mysql_delete: mysqlDeleteTool,
|
||||
mysql_execute: mysqlExecuteTool,
|
||||
neo4j_query: neo4jQueryTool,
|
||||
neo4j_create: neo4jCreateTool,
|
||||
neo4j_merge: neo4jMergeTool,
|
||||
neo4j_update: neo4jUpdateTool,
|
||||
neo4j_delete: neo4jDeleteTool,
|
||||
neo4j_execute: neo4jExecuteTool,
|
||||
github_pr: githubPrTool,
|
||||
github_comment: githubCommentTool,
|
||||
github_issue_comment: githubIssueCommentTool,
|
||||
|
||||
4
apps/sim/triggers/calendly/index.ts
Normal file
4
apps/sim/triggers/calendly/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export { calendlyInviteeCanceledTrigger } from './invitee_canceled'
|
||||
export { calendlyInviteeCreatedTrigger } from './invitee_created'
|
||||
export { calendlyRoutingFormSubmittedTrigger } from './routing_form_submitted'
|
||||
export { calendlyWebhookTrigger } from './webhook'
|
||||
88
apps/sim/triggers/calendly/invitee_canceled.ts
Normal file
88
apps/sim/triggers/calendly/invitee_canceled.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { CalendlyIcon } from '@/components/icons'
|
||||
import type { TriggerConfig } from '@/triggers/types'
|
||||
import { buildInviteeOutputs } from './utils'
|
||||
|
||||
export const calendlyInviteeCanceledTrigger: TriggerConfig = {
|
||||
id: 'calendly_invitee_canceled',
|
||||
name: 'Calendly Invitee Canceled',
|
||||
provider: 'calendly',
|
||||
description: 'Trigger workflow when someone cancels a scheduled event on Calendly',
|
||||
version: '1.0.0',
|
||||
icon: CalendlyIcon,
|
||||
|
||||
subBlocks: [
|
||||
{
|
||||
id: 'apiKey',
|
||||
title: 'Personal Access Token',
|
||||
type: 'short-input',
|
||||
placeholder: 'Enter your Calendly personal access token',
|
||||
password: true,
|
||||
required: true,
|
||||
mode: 'trigger',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'calendly_invitee_canceled',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'organization',
|
||||
title: 'Organization URI',
|
||||
type: 'short-input',
|
||||
placeholder: 'https://api.calendly.com/organizations/XXXXXX',
|
||||
description:
|
||||
'Organization URI for the webhook subscription. Get this from "Get Current User" operation.',
|
||||
required: true,
|
||||
mode: 'trigger',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'calendly_invitee_canceled',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: [
|
||||
'<strong>Note:</strong> This trigger requires a paid Calendly subscription (Professional, Teams, or Enterprise plan).',
|
||||
'Get your Personal Access Token from <strong>Settings > Integrations > API & Webhooks</strong> in your Calendly account.',
|
||||
'Use the "Get Current User" operation in a Calendly block to retrieve your Organization URI.',
|
||||
'The webhook will be automatically created in Calendly when you save this trigger.',
|
||||
'This webhook triggers when an invitee cancels an event. The payload includes cancellation details and reason.',
|
||||
]
|
||||
.map(
|
||||
(instruction, index) =>
|
||||
`<div class="mb-3"><strong>${index + 1}.</strong> ${instruction}</div>`
|
||||
)
|
||||
.join(''),
|
||||
mode: 'trigger',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'calendly_invitee_canceled',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'calendly_invitee_canceled',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'calendly_invitee_canceled',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
outputs: buildInviteeOutputs(),
|
||||
|
||||
webhook: {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Calendly-Webhook-Signature': 'v1,signature...',
|
||||
'User-Agent': 'Calendly-Webhook',
|
||||
},
|
||||
},
|
||||
}
|
||||
97
apps/sim/triggers/calendly/invitee_created.ts
Normal file
97
apps/sim/triggers/calendly/invitee_created.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import { CalendlyIcon } from '@/components/icons'
|
||||
import type { TriggerConfig } from '@/triggers/types'
|
||||
import { buildInviteeOutputs, calendlyTriggerOptions } from './utils'
|
||||
|
||||
export const calendlyInviteeCreatedTrigger: TriggerConfig = {
|
||||
id: 'calendly_invitee_created',
|
||||
name: 'Calendly Invitee Created',
|
||||
provider: 'calendly',
|
||||
description: 'Trigger workflow when someone schedules a new event on Calendly',
|
||||
version: '1.0.0',
|
||||
icon: CalendlyIcon,
|
||||
|
||||
subBlocks: [
|
||||
{
|
||||
id: 'selectedTriggerId',
|
||||
title: 'Trigger Type',
|
||||
type: 'dropdown',
|
||||
mode: 'trigger',
|
||||
options: calendlyTriggerOptions,
|
||||
value: () => 'calendly_invitee_created',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'apiKey',
|
||||
title: 'Personal Access Token',
|
||||
type: 'short-input',
|
||||
placeholder: 'Enter your Calendly personal access token',
|
||||
password: true,
|
||||
required: true,
|
||||
mode: 'trigger',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'calendly_invitee_created',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'organization',
|
||||
title: 'Organization URI',
|
||||
type: 'short-input',
|
||||
placeholder: 'https://api.calendly.com/organizations/XXXXXX',
|
||||
description:
|
||||
'Organization URI for the webhook subscription. Get this from "Get Current User" operation.',
|
||||
required: true,
|
||||
mode: 'trigger',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'calendly_invitee_created',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: [
|
||||
'<strong>Note:</strong> This trigger requires a paid Calendly subscription (Professional, Teams, or Enterprise plan).',
|
||||
'Get your Personal Access Token from <strong>Settings > Integrations > API & Webhooks</strong> in your Calendly account.',
|
||||
'Use the "Get Current User" operation in a Calendly block to retrieve your Organization URI.',
|
||||
'The webhook will be automatically created in Calendly when you save this trigger.',
|
||||
'This webhook triggers when an invitee schedules a new event. Rescheduling triggers both cancellation and creation events.',
|
||||
]
|
||||
.map(
|
||||
(instruction, index) =>
|
||||
`<div class="mb-3"><strong>${index + 1}.</strong> ${instruction}</div>`
|
||||
)
|
||||
.join(''),
|
||||
mode: 'trigger',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'calendly_invitee_created',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'calendly_invitee_created',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'calendly_invitee_created',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
outputs: buildInviteeOutputs(),
|
||||
|
||||
webhook: {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Calendly-Webhook-Signature': 'v1,signature...',
|
||||
'User-Agent': 'Calendly-Webhook',
|
||||
},
|
||||
},
|
||||
}
|
||||
88
apps/sim/triggers/calendly/routing_form_submitted.ts
Normal file
88
apps/sim/triggers/calendly/routing_form_submitted.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { CalendlyIcon } from '@/components/icons'
|
||||
import type { TriggerConfig } from '@/triggers/types'
|
||||
import { buildRoutingFormOutputs } from './utils'
|
||||
|
||||
export const calendlyRoutingFormSubmittedTrigger: TriggerConfig = {
|
||||
id: 'calendly_routing_form_submitted',
|
||||
name: 'Calendly Routing Form Submitted',
|
||||
provider: 'calendly',
|
||||
description: 'Trigger workflow when someone submits a Calendly routing form',
|
||||
version: '1.0.0',
|
||||
icon: CalendlyIcon,
|
||||
|
||||
subBlocks: [
|
||||
{
|
||||
id: 'apiKey',
|
||||
title: 'Personal Access Token',
|
||||
type: 'short-input',
|
||||
placeholder: 'Enter your Calendly personal access token',
|
||||
password: true,
|
||||
required: true,
|
||||
mode: 'trigger',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'calendly_routing_form_submitted',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'organization',
|
||||
title: 'Organization URI',
|
||||
type: 'short-input',
|
||||
placeholder: 'https://api.calendly.com/organizations/XXXXXX',
|
||||
description:
|
||||
'Organization URI for the webhook subscription. Get this from "Get Current User" operation.',
|
||||
required: true,
|
||||
mode: 'trigger',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'calendly_routing_form_submitted',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: [
|
||||
'<strong>Note:</strong> This trigger requires a paid Calendly subscription (Professional, Teams, or Enterprise plan).',
|
||||
'Get your Personal Access Token from <strong>Settings > Integrations > API & Webhooks</strong> in your Calendly account.',
|
||||
'Use the "Get Current User" operation in a Calendly block to retrieve your Organization URI.',
|
||||
'The webhook will be automatically created in Calendly when you save this trigger.',
|
||||
'This webhook triggers when someone submits a routing form, regardless of whether they book an event.',
|
||||
]
|
||||
.map(
|
||||
(instruction, index) =>
|
||||
`<div class="mb-3"><strong>${index + 1}.</strong> ${instruction}</div>`
|
||||
)
|
||||
.join(''),
|
||||
mode: 'trigger',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'calendly_routing_form_submitted',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'calendly_routing_form_submitted',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'calendly_routing_form_submitted',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
outputs: buildRoutingFormOutputs(),
|
||||
|
||||
webhook: {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Calendly-Webhook-Signature': 'v1,signature...',
|
||||
'User-Agent': 'Calendly-Webhook',
|
||||
},
|
||||
},
|
||||
}
|
||||
341
apps/sim/triggers/calendly/utils.ts
Normal file
341
apps/sim/triggers/calendly/utils.ts
Normal file
@@ -0,0 +1,341 @@
|
||||
import type { TriggerOutput } from '@/triggers/types'
|
||||
|
||||
/**
|
||||
* Shared trigger dropdown options for all Calendly triggers
|
||||
*/
|
||||
export const calendlyTriggerOptions = [
|
||||
{ label: 'Invitee Created', id: 'calendly_invitee_created' },
|
||||
{ label: 'Invitee Canceled', id: 'calendly_invitee_canceled' },
|
||||
{ label: 'Routing Form Submitted', id: 'calendly_routing_form_submitted' },
|
||||
{ label: 'General Webhook (All Events)', id: 'calendly_webhook' },
|
||||
]
|
||||
|
||||
/**
|
||||
* Generate setup instructions for a specific Calendly event type
|
||||
*/
|
||||
export function calendlySetupInstructions(eventType: string, additionalNotes?: string): string {
|
||||
const instructions = [
|
||||
'<strong>Note:</strong> Webhooks require a paid Calendly subscription (Professional, Teams, or Enterprise plan).',
|
||||
'<strong>Important:</strong> Calendly does not provide a UI for creating webhooks. You must create them programmatically using the API.',
|
||||
'Get your Calendly <strong>Personal Access Token</strong> from the Calendly dashboard under <strong>Integrations > API & Webhooks</strong>.',
|
||||
'In your workflow, add a Calendly block and select the <strong>"Create Webhook"</strong> operation.',
|
||||
'Enter your Personal Access Token in the Calendly block.',
|
||||
'Copy the <strong>Webhook URL</strong> shown above and paste it into the webhook URL field in the Create Webhook operation.',
|
||||
`Select the event types to monitor. For this trigger, select <strong>${eventType}</strong>.`,
|
||||
'Set the scope to <strong>Organization</strong> or <strong>User</strong> as needed (routing form submissions require organization scope).',
|
||||
'Run the workflow to create the webhook subscription. You can use the "List Webhooks" operation to verify it was created.',
|
||||
]
|
||||
|
||||
if (additionalNotes) {
|
||||
instructions.push(additionalNotes)
|
||||
}
|
||||
|
||||
return instructions
|
||||
.map(
|
||||
(instruction, index) =>
|
||||
`<div class="mb-3">${index === 0 ? instruction : `<strong>${index}.</strong> ${instruction}`}</div>`
|
||||
)
|
||||
.join('')
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared tracking output schema
|
||||
*/
|
||||
export const trackingOutputs = {
|
||||
utm_campaign: {
|
||||
type: 'string',
|
||||
description: 'UTM campaign parameter',
|
||||
},
|
||||
utm_source: {
|
||||
type: 'string',
|
||||
description: 'UTM source parameter',
|
||||
},
|
||||
utm_medium: {
|
||||
type: 'string',
|
||||
description: 'UTM medium parameter',
|
||||
},
|
||||
utm_content: {
|
||||
type: 'string',
|
||||
description: 'UTM content parameter',
|
||||
},
|
||||
utm_term: {
|
||||
type: 'string',
|
||||
description: 'UTM term parameter',
|
||||
},
|
||||
salesforce_uuid: {
|
||||
type: 'string',
|
||||
description: 'Salesforce UUID',
|
||||
},
|
||||
} as const
|
||||
|
||||
/**
|
||||
* Shared questions and answers output schema
|
||||
*/
|
||||
export const questionsAndAnswersOutputs = {
|
||||
type: 'array',
|
||||
description: 'Questions and answers from the booking form',
|
||||
items: {
|
||||
question: {
|
||||
type: 'string',
|
||||
description: 'Question text',
|
||||
},
|
||||
answer: {
|
||||
type: 'string',
|
||||
description: 'Answer text',
|
||||
},
|
||||
},
|
||||
} as const
|
||||
|
||||
/**
|
||||
* Build output schema for invitee events
|
||||
*/
|
||||
export function buildInviteeOutputs(): Record<string, TriggerOutput> {
|
||||
return {
|
||||
event: {
|
||||
type: 'string',
|
||||
description: 'Event type (invitee.created or invitee.canceled)',
|
||||
},
|
||||
created_at: {
|
||||
type: 'string',
|
||||
description: 'Webhook event creation timestamp',
|
||||
},
|
||||
created_by: {
|
||||
type: 'string',
|
||||
description: 'URI of the Calendly user who created this webhook',
|
||||
},
|
||||
payload: {
|
||||
uri: {
|
||||
type: 'string',
|
||||
description: 'Invitee URI',
|
||||
},
|
||||
email: {
|
||||
type: 'string',
|
||||
description: 'Invitee email address',
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'Invitee full name',
|
||||
},
|
||||
first_name: {
|
||||
type: 'string',
|
||||
description: 'Invitee first name',
|
||||
},
|
||||
last_name: {
|
||||
type: 'string',
|
||||
description: 'Invitee last name',
|
||||
},
|
||||
status: {
|
||||
type: 'string',
|
||||
description: 'Invitee status (active or canceled)',
|
||||
},
|
||||
timezone: {
|
||||
type: 'string',
|
||||
description: 'Invitee timezone',
|
||||
},
|
||||
event: {
|
||||
type: 'string',
|
||||
description: 'Scheduled event URI',
|
||||
},
|
||||
questions_and_answers: questionsAndAnswersOutputs,
|
||||
tracking: trackingOutputs,
|
||||
text_reminder_number: {
|
||||
type: 'string',
|
||||
description: 'Phone number for text reminders',
|
||||
},
|
||||
rescheduled: {
|
||||
type: 'boolean',
|
||||
description: 'Whether this invitee rescheduled',
|
||||
},
|
||||
old_invitee: {
|
||||
type: 'string',
|
||||
description: 'URI of the old invitee (if rescheduled)',
|
||||
},
|
||||
new_invitee: {
|
||||
type: 'string',
|
||||
description: 'URI of the new invitee (if rescheduled)',
|
||||
},
|
||||
cancel_url: {
|
||||
type: 'string',
|
||||
description: 'URL to cancel the event',
|
||||
},
|
||||
reschedule_url: {
|
||||
type: 'string',
|
||||
description: 'URL to reschedule the event',
|
||||
},
|
||||
created_at: {
|
||||
type: 'string',
|
||||
description: 'Invitee creation timestamp',
|
||||
},
|
||||
updated_at: {
|
||||
type: 'string',
|
||||
description: 'Invitee last update timestamp',
|
||||
},
|
||||
canceled: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the event was canceled',
|
||||
},
|
||||
cancellation: {
|
||||
type: 'object',
|
||||
description: 'Cancellation details',
|
||||
properties: {
|
||||
canceled_by: {
|
||||
type: 'string',
|
||||
description: 'Who canceled the event',
|
||||
},
|
||||
reason: {
|
||||
type: 'string',
|
||||
description: 'Cancellation reason',
|
||||
},
|
||||
},
|
||||
},
|
||||
payment: {
|
||||
type: 'object',
|
||||
description: 'Payment details',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
description: 'Payment ID',
|
||||
},
|
||||
provider: {
|
||||
type: 'string',
|
||||
description: 'Payment provider',
|
||||
},
|
||||
amount: {
|
||||
type: 'number',
|
||||
description: 'Payment amount',
|
||||
},
|
||||
currency: {
|
||||
type: 'string',
|
||||
description: 'Payment currency',
|
||||
},
|
||||
terms: {
|
||||
type: 'string',
|
||||
description: 'Payment terms',
|
||||
},
|
||||
successful: {
|
||||
type: 'boolean',
|
||||
description: 'Whether payment was successful',
|
||||
},
|
||||
},
|
||||
},
|
||||
no_show: {
|
||||
type: 'object',
|
||||
description: 'No-show details',
|
||||
properties: {
|
||||
created_at: {
|
||||
type: 'string',
|
||||
description: 'No-show marked timestamp',
|
||||
},
|
||||
},
|
||||
},
|
||||
reconfirmation: {
|
||||
type: 'object',
|
||||
description: 'Reconfirmation details',
|
||||
properties: {
|
||||
created_at: {
|
||||
type: 'string',
|
||||
description: 'Reconfirmation timestamp',
|
||||
},
|
||||
confirmed_at: {
|
||||
type: 'string',
|
||||
description: 'Confirmation timestamp',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as any
|
||||
}
|
||||
|
||||
/**
|
||||
* Build output schema for routing form submission events
|
||||
*/
|
||||
export function buildRoutingFormOutputs(): Record<string, TriggerOutput> {
|
||||
return {
|
||||
event: {
|
||||
type: 'string',
|
||||
description: 'Event type (routing_form_submission.created)',
|
||||
},
|
||||
created_at: {
|
||||
type: 'string',
|
||||
description: 'Webhook event creation timestamp',
|
||||
},
|
||||
created_by: {
|
||||
type: 'string',
|
||||
description: 'URI of the Calendly user who created this webhook',
|
||||
},
|
||||
payload: {
|
||||
uri: {
|
||||
type: 'string',
|
||||
description: 'Routing form submission URI',
|
||||
},
|
||||
routing_form: {
|
||||
type: 'string',
|
||||
description: 'Routing form URI',
|
||||
},
|
||||
submitter: {
|
||||
type: 'object',
|
||||
description: 'Submitter details',
|
||||
properties: {
|
||||
uri: {
|
||||
type: 'string',
|
||||
description: 'Submitter URI',
|
||||
},
|
||||
email: {
|
||||
type: 'string',
|
||||
description: 'Submitter email address',
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'Submitter full name',
|
||||
},
|
||||
},
|
||||
},
|
||||
submitter_type: {
|
||||
type: 'string',
|
||||
description: 'Type of submitter',
|
||||
},
|
||||
questions_and_answers: questionsAndAnswersOutputs,
|
||||
tracking: trackingOutputs,
|
||||
result: {
|
||||
type: 'object',
|
||||
description: 'Routing result details',
|
||||
properties: {
|
||||
type: {
|
||||
type: 'string',
|
||||
description: 'Result type (event_type, custom_message, or external_url)',
|
||||
},
|
||||
value: {
|
||||
type: 'string',
|
||||
description: 'Result value (event type URI, message, or URL)',
|
||||
},
|
||||
},
|
||||
},
|
||||
created_at: {
|
||||
type: 'string',
|
||||
description: 'Submission creation timestamp',
|
||||
},
|
||||
updated_at: {
|
||||
type: 'string',
|
||||
description: 'Submission last update timestamp',
|
||||
},
|
||||
},
|
||||
} as any
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a Calendly event matches the expected trigger configuration
|
||||
*/
|
||||
export function isCalendlyEventMatch(triggerId: string, eventType: string): boolean {
|
||||
const eventMap: Record<string, string> = {
|
||||
calendly_invitee_created: 'invitee.created',
|
||||
calendly_invitee_canceled: 'invitee.canceled',
|
||||
calendly_routing_form_submitted: 'routing_form_submission.created',
|
||||
}
|
||||
|
||||
const expectedEvent = eventMap[triggerId]
|
||||
if (!expectedEvent) {
|
||||
return true // Unknown trigger or general webhook, allow through
|
||||
}
|
||||
|
||||
return expectedEvent === eventType
|
||||
}
|
||||
105
apps/sim/triggers/calendly/webhook.ts
Normal file
105
apps/sim/triggers/calendly/webhook.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import { CalendlyIcon } from '@/components/icons'
|
||||
import type { TriggerConfig } from '@/triggers/types'
|
||||
|
||||
export const calendlyWebhookTrigger: TriggerConfig = {
|
||||
id: 'calendly_webhook',
|
||||
name: 'Calendly Webhook',
|
||||
provider: 'calendly',
|
||||
description: 'Trigger workflow from any Calendly webhook event',
|
||||
version: '1.0.0',
|
||||
icon: CalendlyIcon,
|
||||
|
||||
subBlocks: [
|
||||
{
|
||||
id: 'apiKey',
|
||||
title: 'Personal Access Token',
|
||||
type: 'short-input',
|
||||
placeholder: 'Enter your Calendly personal access token',
|
||||
password: true,
|
||||
required: true,
|
||||
mode: 'trigger',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'calendly_webhook',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'organization',
|
||||
title: 'Organization URI',
|
||||
type: 'short-input',
|
||||
placeholder: 'https://api.calendly.com/organizations/XXXXXX',
|
||||
description:
|
||||
'Organization URI for the webhook subscription. Get this from "Get Current User" operation.',
|
||||
required: true,
|
||||
mode: 'trigger',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'calendly_webhook',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: [
|
||||
'<strong>Note:</strong> This trigger requires a paid Calendly subscription (Professional, Teams, or Enterprise plan).',
|
||||
'Get your Personal Access Token from <strong>Settings > Integrations > API & Webhooks</strong> in your Calendly account.',
|
||||
'Use the "Get Current User" operation in a Calendly block to retrieve your Organization URI.',
|
||||
'The webhook will be automatically created in Calendly when you save this trigger.',
|
||||
'This webhook subscribes to all Calendly events (invitee created, invitee canceled, and routing form submitted). Use the <code>event</code> field in the payload to determine the event type.',
|
||||
]
|
||||
.map(
|
||||
(instruction, index) =>
|
||||
`<div class="mb-3"><strong>${index + 1}.</strong> ${instruction}</div>`
|
||||
)
|
||||
.join(''),
|
||||
mode: 'trigger',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'calendly_webhook',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'calendly_webhook',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'calendly_webhook',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
outputs: {
|
||||
event: {
|
||||
type: 'string',
|
||||
description:
|
||||
'Event type (invitee.created, invitee.canceled, or routing_form_submission.created)',
|
||||
},
|
||||
created_at: {
|
||||
type: 'string',
|
||||
description: 'Webhook event creation timestamp',
|
||||
},
|
||||
created_by: {
|
||||
type: 'string',
|
||||
description: 'URI of the Calendly user who created this webhook',
|
||||
},
|
||||
payload: {
|
||||
type: 'object',
|
||||
description: 'Complete event payload (structure varies by event type)',
|
||||
},
|
||||
},
|
||||
|
||||
webhook: {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Calendly-Webhook-Signature': 'v1,signature...',
|
||||
'User-Agent': 'Calendly-Webhook',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1,4 +1,10 @@
|
||||
import { airtableWebhookTrigger } from '@/triggers/airtable'
|
||||
import {
|
||||
calendlyInviteeCanceledTrigger,
|
||||
calendlyInviteeCreatedTrigger,
|
||||
calendlyRoutingFormSubmittedTrigger,
|
||||
calendlyWebhookTrigger,
|
||||
} from '@/triggers/calendly'
|
||||
import { genericWebhookTrigger } from '@/triggers/generic'
|
||||
import {
|
||||
githubIssueClosedTrigger,
|
||||
@@ -83,6 +89,10 @@ import { whatsappWebhookTrigger } from '@/triggers/whatsapp'
|
||||
export const TRIGGER_REGISTRY: TriggerRegistry = {
|
||||
slack_webhook: slackWebhookTrigger,
|
||||
airtable_webhook: airtableWebhookTrigger,
|
||||
calendly_webhook: calendlyWebhookTrigger,
|
||||
calendly_invitee_created: calendlyInviteeCreatedTrigger,
|
||||
calendly_invitee_canceled: calendlyInviteeCanceledTrigger,
|
||||
calendly_routing_form_submitted: calendlyRoutingFormSubmittedTrigger,
|
||||
generic_webhook: genericWebhookTrigger,
|
||||
github_webhook: githubWebhookTrigger,
|
||||
github_issue_opened: githubIssueOpenedTrigger,
|
||||
|
||||
30
bun.lock
30
bun.lock
@@ -12,6 +12,7 @@
|
||||
"cronstrue": "3.3.0",
|
||||
"drizzle-orm": "^0.44.5",
|
||||
"mongodb": "6.19.0",
|
||||
"neo4j-driver": "6.0.1",
|
||||
"onedollarstats": "0.0.10",
|
||||
"postgres": "^3.4.5",
|
||||
"remark-gfm": "4.0.1",
|
||||
@@ -113,6 +114,7 @@
|
||||
"entities": "6.0.1",
|
||||
"framer-motion": "^12.5.0",
|
||||
"fuse.js": "7.1.0",
|
||||
"fuzzysort": "3.1.0",
|
||||
"gray-matter": "^4.0.3",
|
||||
"groq-sdk": "^0.15.0",
|
||||
"html-to-text": "^9.0.5",
|
||||
@@ -1519,7 +1521,7 @@
|
||||
|
||||
"bson": ["bson@6.10.4", "", {}, "sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng=="],
|
||||
|
||||
"buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="],
|
||||
"buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="],
|
||||
|
||||
"buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="],
|
||||
|
||||
@@ -1953,6 +1955,8 @@
|
||||
|
||||
"fuse.js": ["fuse.js@7.1.0", "", {}, "sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ=="],
|
||||
|
||||
"fuzzysort": ["fuzzysort@3.1.0", "", {}, "sha512-sR9BNCjBg6LNgwvxlBd0sBABvQitkLzoVY9MYYROQVX/FvfJ4Mai9LsGhDgd8qYdds0bY77VzYd5iuB+v5rwQQ=="],
|
||||
|
||||
"gaxios": ["gaxios@6.7.1", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "is-stream": "^2.0.0", "node-fetch": "^2.6.9", "uuid": "^9.0.1" } }, "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ=="],
|
||||
|
||||
"gcp-metadata": ["gcp-metadata@6.1.1", "", { "dependencies": { "gaxios": "^6.1.1", "google-logging-utils": "^0.0.2", "json-bigint": "^1.0.0" } }, "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A=="],
|
||||
@@ -2445,6 +2449,12 @@
|
||||
|
||||
"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
|
||||
|
||||
"neo4j-driver": ["neo4j-driver@6.0.1", "", { "dependencies": { "neo4j-driver-bolt-connection": "6.0.1", "neo4j-driver-core": "6.0.1", "rxjs": "^7.8.2" } }, "sha512-8DDF2MwEJNz7y7cp97x4u8fmVIP4CWS8qNBxdwxTG0fWtsS+2NdeC+7uXwmmuFOpHvkfXqv63uWY73bfDtOH8Q=="],
|
||||
|
||||
"neo4j-driver-bolt-connection": ["neo4j-driver-bolt-connection@6.0.1", "", { "dependencies": { "buffer": "^6.0.3", "neo4j-driver-core": "6.0.1", "string_decoder": "^1.3.0" } }, "sha512-1KyG73TO+CwnYJisdHD0sjUw9yR+P5q3JFcmVPzsHT4/whzCjuXSMpmY4jZcHH2PdY2cBUq4l/6WcDiPMxW2UA=="],
|
||||
|
||||
"neo4j-driver-core": ["neo4j-driver-core@6.0.1", "", {}, "sha512-5I2KxICAvcHxnWdJyDqwu8PBAQvWVTlQH2ve3VQmtVdJScPqWhpXN1PiX5IIl+cRF3pFpz9GQF53B5n6s0QQUQ=="],
|
||||
|
||||
"next": ["next@15.4.1", "", { "dependencies": { "@next/env": "15.4.1", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.4.1", "@next/swc-darwin-x64": "15.4.1", "@next/swc-linux-arm64-gnu": "15.4.1", "@next/swc-linux-arm64-musl": "15.4.1", "@next/swc-linux-x64-gnu": "15.4.1", "@next/swc-linux-x64-musl": "15.4.1", "@next/swc-win32-arm64-msvc": "15.4.1", "@next/swc-win32-x64-msvc": "15.4.1", "sharp": "^0.34.3" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-eNKB1q8C7o9zXF8+jgJs2CzSLIU3T6bQtX6DcTnCq1sIR1CJ0GlSyRs1BubQi3/JgCnr9Vr+rS5mOMI38FFyQw=="],
|
||||
|
||||
"next-mdx-remote": ["next-mdx-remote@5.0.0", "", { "dependencies": { "@babel/code-frame": "^7.23.5", "@mdx-js/mdx": "^3.0.1", "@mdx-js/react": "^3.0.1", "unist-util-remove": "^3.1.0", "vfile": "^6.0.1", "vfile-matter": "^5.0.0" }, "peerDependencies": { "react": ">=16" } }, "sha512-RNNbqRpK9/dcIFZs/esQhuLA8jANqlH694yqoDBK8hkVdJUndzzGmnPHa2nyi90N4Z9VmzuSWNRpr5ItT3M7xQ=="],
|
||||
@@ -2893,7 +2903,7 @@
|
||||
|
||||
"string.prototype.codepointat": ["string.prototype.codepointat@0.2.1", "", {}, "sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg=="],
|
||||
|
||||
"string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
|
||||
"string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
|
||||
|
||||
"stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="],
|
||||
|
||||
@@ -3425,6 +3435,8 @@
|
||||
|
||||
"better-auth/zod": ["zod@4.1.11", "", {}, "sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg=="],
|
||||
|
||||
"bl/buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="],
|
||||
|
||||
"bl/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
|
||||
|
||||
"chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
||||
@@ -3577,6 +3589,8 @@
|
||||
|
||||
"react-promise-suspense/fast-deep-equal": ["fast-deep-equal@2.0.1", "", {}, "sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w=="],
|
||||
|
||||
"readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
|
||||
|
||||
"readable-web-to-node-stream/readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="],
|
||||
|
||||
"resend/@react-email/render": ["@react-email/render@1.1.2", "", { "dependencies": { "html-to-text": "^9.0.5", "prettier": "^3.5.3", "react-promise-suspense": "^0.3.4" }, "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-RnRehYN3v9gVlNMehHPHhyp2RQo7+pSkHDtXPvg3s0GbzM9SQMW4Qrf8GRNvtpLC4gsI+Wt0VatNRUFqjvevbw=="],
|
||||
@@ -3619,6 +3633,8 @@
|
||||
|
||||
"string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||
|
||||
"string_decoder/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
||||
|
||||
"sucrase/commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="],
|
||||
|
||||
"sucrase/glob": ["glob@10.4.5", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="],
|
||||
@@ -3757,10 +3773,14 @@
|
||||
|
||||
"accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||
|
||||
"bl/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
|
||||
|
||||
"cli-truncate/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
|
||||
|
||||
"cli-truncate/string-width/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="],
|
||||
|
||||
"concat-stream/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
|
||||
|
||||
"engine.io/@types/node/undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="],
|
||||
|
||||
"form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||
@@ -3857,10 +3877,6 @@
|
||||
|
||||
"protobufjs/@types/node/undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="],
|
||||
|
||||
"readable-web-to-node-stream/readable-stream/buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="],
|
||||
|
||||
"readable-web-to-node-stream/readable-stream/string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
|
||||
|
||||
"resend/@react-email/render/prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="],
|
||||
|
||||
"restore-cursor/onetime/mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="],
|
||||
@@ -3971,8 +3987,6 @@
|
||||
|
||||
"ora/cli-cursor/restore-cursor/onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="],
|
||||
|
||||
"readable-web-to-node-stream/readable-stream/string_decoder/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
||||
|
||||
"sim/tailwindcss/chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||
|
||||
"sim/tailwindcss/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
"cronstrue": "3.3.0",
|
||||
"drizzle-orm": "^0.44.5",
|
||||
"mongodb": "6.19.0",
|
||||
"neo4j-driver": "6.0.1",
|
||||
"onedollarstats": "0.0.10",
|
||||
"postgres": "^3.4.5",
|
||||
"remark-gfm": "4.0.1",
|
||||
|
||||
Reference in New Issue
Block a user