feat(selectors): add dropdown selectors for 14 integrations (#3433)

* feat(selectors): add dropdown selectors for 14 integrations

* fix(selectors): secure OAuth tokens in JSM and Confluence selector routes

Convert JSM selector-servicedesks, selector-requesttypes, and Confluence
selector-spaces routes from GET (with access token in URL query params) to
POST with authorizeCredentialUse + refreshAccessTokenIfNeeded pattern. Also
adds missing ensureCredential guard to microsoft.planner.plans registry entry.

* fix(selectors): use sanitized serviceDeskId and encode SharePoint siteId

Use serviceDeskIdValidation.sanitized instead of raw serviceDeskId in JSM
request types URL. Add encodeURIComponent to SharePoint siteId to prevent
URL path injection.

* lint

* fix(selectors): revert encodeURIComponent on SharePoint siteId

SharePoint site IDs use the format "hostname,guid,guid" with commas that
must remain unencoded for the Microsoft Graph API. The encodeURIComponent
call would convert commas to %2C and break the API call.

* fix(selectors): use sanitized cloudId in Confluence and JSM route URLs

Use cloudIdValidation.sanitized instead of raw cloudId in URL construction
for consistency with the validation pattern, even though the current
validator returns the input unchanged.

* fix(selectors): add missing context fields to resolution, ensureCredential to sharepoint.lists, and siteId validation

- Add baseId, datasetId, serviceDeskId to SelectorResolutionArgs,
  ExtendedSelectorContext, extractExtendedContext, useSelectorDisplayName,
  and resolveSelectorForSubBlock so cascading selectors resolve correctly
  through the resolution path.
- Add ensureCredential guard to sharepoint.lists registry entry.
- Add regex validation for SharePoint siteId format (hostname,GUID,GUID).

* fix(selectors): rename advanced subBlock IDs to avoid canonicalParamId clashes

Rename all advanced-mode subBlock IDs that matched their canonicalParamId
to use a `manual*` prefix, following the established convention
(e.g., manualSiteId, manualCredential). This prevents ambiguity between
subBlock IDs and canonical parameter names in the serialization layer.

25 renames across 14 blocks: baseId→manualBaseId, tableId→manualTableId,
workspace→manualWorkspace, objectType→manualObjectType, etc.

* Revert "fix(selectors): rename advanced subBlock IDs to avoid canonicalParamId clashes"

This reverts commit 4e30161c68.

* fix(selectors): rename canonicalParamIds to avoid subBlock ID clashes

Prefix all clashing canonicalParamId values with `selected_` so they
don't match any subBlock ID. Update each block's `inputs` section and
`tools.config.params` function to destructure the new canonical names
and remap them to the original tool param names. SubBlock IDs and tool
definitions remain unchanged for backwards compatibility.

Affected: 25 canonical params across 14 blocks (airtable, asana, attio,
calcom, confluence, google_bigquery, google_tasks, jsm, microsoft_planner,
notion, pipedrive, sharepoint, trello, zoom).

* fix(selectors): rename pre-existing driveId and files canonicalParamIds in SharePoint

Apply the same selected_ prefix convention to the pre-existing SharePoint
driveId and files canonical params that clashed with their subBlock IDs.

* style: format long lines in calcom, pipedrive, and sharepoint blocks

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(selectors): resolve cascading context for selected_ canonical params and normalize Asana response

Strip `selected_` prefix from canonical param IDs when mapping to
SelectorContext fields so cascading selectors (Airtable base→table,
BigQuery dataset→table, JSM serviceDesk→requestType) correctly
propagate parent values.

Normalize Asana workspaces route to return `{ id, name }` instead of
`{ gid, name }` for consistency with all other selector routes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(selectors): replace hacky prefix stripping with explicit CANONICAL_TO_CONTEXT mapping

Replace CONTEXT_FIELD_SET (Record<string, true>) with CANONICAL_TO_CONTEXT
(Record<string, keyof SelectorContext>) that explicitly maps canonical
param IDs to their SelectorContext field names.

This properly handles the selected_ prefix aliases (e.g. selected_baseId
→ baseId) without string manipulation, and removes the unsafe
Record<string, unknown> cast.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor(selectors): remove unnecessary selected_ prefix from canonicalParamIds

The selected_ prefix was added to avoid a perceived clash between
canonicalParamId and subBlock id values, but this clash does not
actually cause any issues — pre-existing blocks on main (Google Sheets,
Webflow, SharePoint) already use matching values successfully.

Remove the prefix from all 14 blocks, revert use-selector-setup.ts to
the simple CONTEXT_FIELD_SET pattern, and simplify tools.config.params
functions that were only remapping the prefix back.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(selectors): add spaceId selector pair to Confluence V2 block

The V2 block was missing the spaceSelector basic-mode selector that the
V1 (Legacy) block already had.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor(selectors): revert V1 block changes, add selectors to Notion V1 for V2 inheritance

Confluence V1: reverted to main state (V2 has its own subBlocks).
Notion V1: added selector pairs per-operation since V2 inherits
subBlocks, inputs, and params from V1.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(selectors): audit fixes for auth patterns, registry gaps, and display name resolution

- Convert Microsoft Planner plans/tasks routes from GET+getSession to POST+authorizeCredentialUse
- Add fetchById to microsoft.planner (tasks) and sharepoint.sites registry entries
- Add ensureCredential to sharepoint.sites and microsoft.planner registry fetchList
- Update microsoft.planner.plans registry to use POST method
- Add siteId, collectionId, spreadsheetId, fileId to SelectorDisplayNameArgs and caller
- Add fileId to SelectorResolutionArgs and resolution context
- Fix Zoom topicUpdate visibility in basic mode (remove mode:'advanced')
- Change Zoom meetings selector to fetch upcoming_meetings instead of only scheduled

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* style: lint formatting fixes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(selectors): consolidate Notion canonical param pairs into array conditions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(selectors): add missing selectorKey to Confluence V1 page selector

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(selectors): use sanitized IDs in URLs, convert SharePoint routes to POST+authorizeCredentialUse

- Use planIdValidation.sanitized in MS Planner tasks fetch URL
- Convert sharepoint/lists and sharepoint/sites from GET+getSession to POST+authorizeCredentialUse
- Update registry entries to match POST pattern

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(selectors): revert Zoom meetings type to scheduled for broader compatibility

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(selectors): add SharePoint site ID validator, fix cascading selector display name fallbacks

- Add validateSharePointSiteId to input-validation.ts
- Use validation util in SharePoint lists route instead of inline regex
- Add || fallback to selector IDs in workflow-block.tsx so cascading
  display names resolve in basic mode (baseSelector, planSelector, etc.)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(selectors): hoist requestId before try block in all selector routes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(selectors): hoist requestId before try block in Trello boards route

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(selectors): guard selector queries against unresolved variable references

Skip fetchById and context population when values are design-time
placeholders (<Block.output> or {{ENV_VAR}}) rather than real IDs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor(selectors): replace hardcoded display name fallbacks with canonical-aware resolution

Use resolveDependencyValue to resolve context values for
useSelectorDisplayName, eliminating manual || getStringValue('*Selector')
fallbacks that required updating for each new selector pair.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(selectors): tighten SharePoint site ID validation to exclude underscores

SharePoint composite site IDs use hostname,guid,guid format where only
alphanumerics, periods, hyphens, and commas are valid characters.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(selectors): ensure string IDs in Pipedrive/Cal.com routes, fix Trello closed board filter

Pipedrive pipelines and Cal.com event-types/schedules routes now
consistently return string IDs via String() conversion.

Trello boards route no longer filters out closed boards, preserving
them for fetchById lookups. The closed filter is applied only in the
registry's fetchList so archived boards don't appear in dropdowns
but can still be resolved by ID for display names.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(selectors): convert Zoom meeting IDs to strings for consistency

Zoom API returns numeric meeting IDs. Convert with String() to match
the string ID convention used by all other selector routes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(selectors): align registry types with route string ID returns

Routes already convert numeric IDs to strings via String(), so update
the registry types (CalcomEventType, CalcomSchedule, PipedrivePipeline,
ZoomMeeting) from id: number to id: string and remove the now-redundant
String() coercions in fetchList/fetchById.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Waleed
2026-03-06 12:34:28 -08:00
committed by GitHub
co-authored by Claude Opus 4.6
parent ae887185a1
commit 244cf4ff7e
46 changed files with 3345 additions and 236 deletions
@@ -0,0 +1,79 @@
import { createLogger } from '@sim/logger'
import { NextResponse } from 'next/server'
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
import { generateRequestId } from '@/lib/core/utils/request'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
const logger = createLogger('AirtableBasesAPI')
export const dynamic = 'force-dynamic'
export async function POST(request: Request) {
const requestId = generateRequestId()
try {
const body = await request.json()
const { credential, workflowId } = body
if (!credential) {
logger.error('Missing credential in request')
return NextResponse.json({ error: 'Credential is required' }, { status: 400 })
}
const authz = await authorizeCredentialUse(request as any, {
credentialId: credential,
workflowId,
})
if (!authz.ok || !authz.credentialOwnerUserId) {
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
}
const accessToken = await refreshAccessTokenIfNeeded(
credential,
authz.credentialOwnerUserId,
requestId
)
if (!accessToken) {
logger.error('Failed to get access token', {
credentialId: credential,
userId: authz.credentialOwnerUserId,
})
return NextResponse.json(
{ error: 'Could not retrieve access token', authRequired: true },
{ status: 401 }
)
}
const response = await fetch('https://api.airtable.com/v0/meta/bases', {
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
})
if (!response.ok) {
const errorData = await response.json().catch(() => ({}))
logger.error('Failed to fetch Airtable bases', {
status: response.status,
error: errorData,
})
return NextResponse.json(
{ error: 'Failed to fetch Airtable bases', details: errorData },
{ status: response.status }
)
}
const data = await response.json()
const bases = (data.bases || []).map((base: { id: string; name: string }) => ({
id: base.id,
name: base.name,
}))
return NextResponse.json({ bases })
} catch (error) {
logger.error('Error processing Airtable bases request:', error)
return NextResponse.json(
{ error: 'Failed to retrieve Airtable bases', details: (error as Error).message },
{ status: 500 }
)
}
}
@@ -0,0 +1,95 @@
import { createLogger } from '@sim/logger'
import { NextResponse } from 'next/server'
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
import { validateAirtableId } from '@/lib/core/security/input-validation'
import { generateRequestId } from '@/lib/core/utils/request'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
const logger = createLogger('AirtableTablesAPI')
export const dynamic = 'force-dynamic'
export async function POST(request: Request) {
const requestId = generateRequestId()
try {
const body = await request.json()
const { credential, workflowId, baseId } = body
if (!credential) {
logger.error('Missing credential in request')
return NextResponse.json({ error: 'Credential is required' }, { status: 400 })
}
if (!baseId) {
logger.error('Missing baseId in request')
return NextResponse.json({ error: 'Base ID is required' }, { status: 400 })
}
const baseIdValidation = validateAirtableId(baseId, 'app', 'baseId')
if (!baseIdValidation.isValid) {
logger.error('Invalid baseId', { error: baseIdValidation.error })
return NextResponse.json({ error: baseIdValidation.error }, { status: 400 })
}
const authz = await authorizeCredentialUse(request as any, {
credentialId: credential,
workflowId,
})
if (!authz.ok || !authz.credentialOwnerUserId) {
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
}
const accessToken = await refreshAccessTokenIfNeeded(
credential,
authz.credentialOwnerUserId,
requestId
)
if (!accessToken) {
logger.error('Failed to get access token', {
credentialId: credential,
userId: authz.credentialOwnerUserId,
})
return NextResponse.json(
{ error: 'Could not retrieve access token', authRequired: true },
{ status: 401 }
)
}
const response = await fetch(
`https://api.airtable.com/v0/meta/bases/${baseIdValidation.sanitized}/tables`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
}
)
if (!response.ok) {
const errorData = await response.json().catch(() => ({}))
logger.error('Failed to fetch Airtable tables', {
status: response.status,
error: errorData,
baseId,
})
return NextResponse.json(
{ error: 'Failed to fetch Airtable tables', details: errorData },
{ status: response.status }
)
}
const data = await response.json()
const tables = (data.tables || []).map((table: { id: string; name: string }) => ({
id: table.id,
name: table.name,
}))
return NextResponse.json({ tables })
} catch (error) {
logger.error('Error processing Airtable tables request:', error)
return NextResponse.json(
{ error: 'Failed to retrieve Airtable tables', details: (error as Error).message },
{ status: 500 }
)
}
}
@@ -0,0 +1,79 @@
import { createLogger } from '@sim/logger'
import { NextResponse } from 'next/server'
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
import { generateRequestId } from '@/lib/core/utils/request'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
const logger = createLogger('AsanaWorkspacesAPI')
export const dynamic = 'force-dynamic'
export async function POST(request: Request) {
const requestId = generateRequestId()
try {
const body = await request.json()
const { credential, workflowId } = body
if (!credential) {
logger.error('Missing credential in request')
return NextResponse.json({ error: 'Credential is required' }, { status: 400 })
}
const authz = await authorizeCredentialUse(request as any, {
credentialId: credential,
workflowId,
})
if (!authz.ok || !authz.credentialOwnerUserId) {
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
}
const accessToken = await refreshAccessTokenIfNeeded(
credential,
authz.credentialOwnerUserId,
requestId
)
if (!accessToken) {
logger.error('Failed to get access token', {
credentialId: credential,
userId: authz.credentialOwnerUserId,
})
return NextResponse.json(
{ error: 'Could not retrieve access token', authRequired: true },
{ status: 401 }
)
}
const response = await fetch('https://app.asana.com/api/1.0/workspaces', {
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: 'application/json',
},
})
if (!response.ok) {
const errorData = await response.json().catch(() => ({}))
logger.error('Failed to fetch Asana workspaces', {
status: response.status,
error: errorData,
})
return NextResponse.json(
{ error: 'Failed to fetch Asana workspaces', details: errorData },
{ status: response.status }
)
}
const data = await response.json()
const workspaces = (data.data || []).map((workspace: { gid: string; name: string }) => ({
id: workspace.gid,
name: workspace.name,
}))
return NextResponse.json({ workspaces })
} catch (error) {
logger.error('Error processing Asana workspaces request:', error)
return NextResponse.json(
{ error: 'Failed to retrieve Asana workspaces', details: (error as Error).message },
{ status: 500 }
)
}
}
@@ -0,0 +1,79 @@
import { createLogger } from '@sim/logger'
import { NextResponse } from 'next/server'
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
import { generateRequestId } from '@/lib/core/utils/request'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
const logger = createLogger('AttioListsAPI')
export const dynamic = 'force-dynamic'
export async function POST(request: Request) {
const requestId = generateRequestId()
try {
const body = await request.json()
const { credential, workflowId } = body
if (!credential) {
logger.error('Missing credential in request')
return NextResponse.json({ error: 'Credential is required' }, { status: 400 })
}
const authz = await authorizeCredentialUse(request as any, {
credentialId: credential,
workflowId,
})
if (!authz.ok || !authz.credentialOwnerUserId) {
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
}
const accessToken = await refreshAccessTokenIfNeeded(
credential,
authz.credentialOwnerUserId,
requestId
)
if (!accessToken) {
logger.error('Failed to get access token', {
credentialId: credential,
userId: authz.credentialOwnerUserId,
})
return NextResponse.json(
{ error: 'Could not retrieve access token', authRequired: true },
{ status: 401 }
)
}
const response = await fetch('https://api.attio.com/v2/lists', {
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: 'application/json',
},
})
if (!response.ok) {
const errorData = await response.json().catch(() => ({}))
logger.error('Failed to fetch Attio lists', {
status: response.status,
error: errorData,
})
return NextResponse.json(
{ error: 'Failed to fetch Attio lists', details: errorData },
{ status: response.status }
)
}
const data = await response.json()
const lists = (data.data || []).map((list: { api_slug: string; name: string }) => ({
id: list.api_slug,
name: list.name,
}))
return NextResponse.json({ lists })
} catch (error) {
logger.error('Error processing Attio lists request:', error)
return NextResponse.json(
{ error: 'Failed to retrieve Attio lists', details: (error as Error).message },
{ status: 500 }
)
}
}
@@ -0,0 +1,79 @@
import { createLogger } from '@sim/logger'
import { NextResponse } from 'next/server'
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
import { generateRequestId } from '@/lib/core/utils/request'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
const logger = createLogger('AttioObjectsAPI')
export const dynamic = 'force-dynamic'
export async function POST(request: Request) {
const requestId = generateRequestId()
try {
const body = await request.json()
const { credential, workflowId } = body
if (!credential) {
logger.error('Missing credential in request')
return NextResponse.json({ error: 'Credential is required' }, { status: 400 })
}
const authz = await authorizeCredentialUse(request as any, {
credentialId: credential,
workflowId,
})
if (!authz.ok || !authz.credentialOwnerUserId) {
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
}
const accessToken = await refreshAccessTokenIfNeeded(
credential,
authz.credentialOwnerUserId,
requestId
)
if (!accessToken) {
logger.error('Failed to get access token', {
credentialId: credential,
userId: authz.credentialOwnerUserId,
})
return NextResponse.json(
{ error: 'Could not retrieve access token', authRequired: true },
{ status: 401 }
)
}
const response = await fetch('https://api.attio.com/v2/objects', {
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: 'application/json',
},
})
if (!response.ok) {
const errorData = await response.json().catch(() => ({}))
logger.error('Failed to fetch Attio objects', {
status: response.status,
error: errorData,
})
return NextResponse.json(
{ error: 'Failed to fetch Attio objects', details: errorData },
{ status: response.status }
)
}
const data = await response.json()
const objects = (data.data || []).map((obj: { api_slug: string; singular_noun: string }) => ({
id: obj.api_slug,
name: obj.singular_noun,
}))
return NextResponse.json({ objects })
} catch (error) {
logger.error('Error processing Attio objects request:', error)
return NextResponse.json(
{ error: 'Failed to retrieve Attio objects', details: (error as Error).message },
{ status: 500 }
)
}
}
@@ -0,0 +1,83 @@
import { createLogger } from '@sim/logger'
import { NextResponse } from 'next/server'
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
import { generateRequestId } from '@/lib/core/utils/request'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
const logger = createLogger('CalcomEventTypesAPI')
export const dynamic = 'force-dynamic'
export async function POST(request: Request) {
const requestId = generateRequestId()
try {
const body = await request.json()
const { credential, workflowId } = body
if (!credential) {
logger.error('Missing credential in request')
return NextResponse.json({ error: 'Credential is required' }, { status: 400 })
}
const authz = await authorizeCredentialUse(request as any, {
credentialId: credential,
workflowId,
})
if (!authz.ok || !authz.credentialOwnerUserId) {
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
}
const accessToken = await refreshAccessTokenIfNeeded(
credential,
authz.credentialOwnerUserId,
requestId
)
if (!accessToken) {
logger.error('Failed to get access token', {
credentialId: credential,
userId: authz.credentialOwnerUserId,
})
return NextResponse.json(
{ error: 'Could not retrieve access token', authRequired: true },
{ status: 401 }
)
}
const response = await fetch('https://api.cal.com/v2/event-types', {
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
'cal-api-version': '2024-06-14',
},
})
if (!response.ok) {
const errorData = await response.json().catch(() => ({}))
logger.error('Failed to fetch Cal.com event types', {
status: response.status,
error: errorData,
})
return NextResponse.json(
{ error: 'Failed to fetch Cal.com event types', details: errorData },
{ status: response.status }
)
}
const data = await response.json()
const eventTypes = (data.data || []).map(
(eventType: { id: number; title: string; slug: string }) => ({
id: String(eventType.id),
title: eventType.title,
slug: eventType.slug,
})
)
return NextResponse.json({ eventTypes })
} catch (error) {
logger.error('Error processing Cal.com event types request:', error)
return NextResponse.json(
{ error: 'Failed to retrieve Cal.com event types', details: (error as Error).message },
{ status: 500 }
)
}
}
@@ -0,0 +1,80 @@
import { createLogger } from '@sim/logger'
import { NextResponse } from 'next/server'
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
import { generateRequestId } from '@/lib/core/utils/request'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
const logger = createLogger('CalcomSchedulesAPI')
export const dynamic = 'force-dynamic'
export async function POST(request: Request) {
const requestId = generateRequestId()
try {
const body = await request.json()
const { credential, workflowId } = body
if (!credential) {
logger.error('Missing credential in request')
return NextResponse.json({ error: 'Credential is required' }, { status: 400 })
}
const authz = await authorizeCredentialUse(request as any, {
credentialId: credential,
workflowId,
})
if (!authz.ok || !authz.credentialOwnerUserId) {
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
}
const accessToken = await refreshAccessTokenIfNeeded(
credential,
authz.credentialOwnerUserId,
requestId
)
if (!accessToken) {
logger.error('Failed to get access token', {
credentialId: credential,
userId: authz.credentialOwnerUserId,
})
return NextResponse.json(
{ error: 'Could not retrieve access token', authRequired: true },
{ status: 401 }
)
}
const response = await fetch('https://api.cal.com/v2/schedules', {
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
'cal-api-version': '2024-06-11',
},
})
if (!response.ok) {
const errorData = await response.json().catch(() => ({}))
logger.error('Failed to fetch Cal.com schedules', {
status: response.status,
error: errorData,
})
return NextResponse.json(
{ error: 'Failed to fetch Cal.com schedules', details: errorData },
{ status: response.status }
)
}
const data = await response.json()
const schedules = (data.data || []).map((schedule: { id: number; name: string }) => ({
id: String(schedule.id),
name: schedule.name,
}))
return NextResponse.json({ schedules })
} catch (error) {
logger.error('Error processing Cal.com schedules request:', error)
return NextResponse.json(
{ error: 'Failed to retrieve Cal.com schedules', details: (error as Error).message },
{ status: 500 }
)
}
}
@@ -0,0 +1,96 @@
import { createLogger } from '@sim/logger'
import { NextResponse } from 'next/server'
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
import { validateJiraCloudId } from '@/lib/core/security/input-validation'
import { generateRequestId } from '@/lib/core/utils/request'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
import { getConfluenceCloudId } from '@/tools/confluence/utils'
const logger = createLogger('ConfluenceSelectorSpacesAPI')
export const dynamic = 'force-dynamic'
export async function POST(request: Request) {
const requestId = generateRequestId()
try {
const body = await request.json()
const { credential, workflowId, domain } = body
if (!credential) {
logger.error('Missing credential in request')
return NextResponse.json({ error: 'Credential is required' }, { status: 400 })
}
if (!domain) {
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
}
const authz = await authorizeCredentialUse(request as any, {
credentialId: credential,
workflowId,
})
if (!authz.ok || !authz.credentialOwnerUserId) {
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
}
const accessToken = await refreshAccessTokenIfNeeded(
credential,
authz.credentialOwnerUserId,
requestId
)
if (!accessToken) {
logger.error('Failed to get access token', {
credentialId: credential,
userId: authz.credentialOwnerUserId,
})
return NextResponse.json(
{ error: 'Could not retrieve access token', authRequired: true },
{ status: 401 }
)
}
const cloudId = await getConfluenceCloudId(domain, accessToken)
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
if (!cloudIdValidation.isValid) {
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
}
const url = `https://api.atlassian.com/ex/confluence/${cloudIdValidation.sanitized}/wiki/api/v2/spaces?limit=250`
const response = await fetch(url, {
method: 'GET',
headers: {
Accept: 'application/json',
Authorization: `Bearer ${accessToken}`,
},
})
if (!response.ok) {
const errorData = await response.json().catch(() => null)
logger.error('Confluence API error:', {
status: response.status,
statusText: response.statusText,
error: errorData,
})
const errorMessage =
errorData?.message || `Failed to list Confluence spaces (${response.status})`
return NextResponse.json({ error: errorMessage }, { status: response.status })
}
const data = await response.json()
const spaces = (data.results || []).map((space: { id: string; name: string; key: string }) => ({
id: space.id,
name: space.name,
key: space.key,
}))
return NextResponse.json({ spaces })
} catch (error) {
logger.error('Error listing Confluence spaces:', error)
return NextResponse.json(
{ error: (error as Error).message || 'Internal server error' },
{ status: 500 }
)
}
}
@@ -0,0 +1,100 @@
import { createLogger } from '@sim/logger'
import { NextResponse } from 'next/server'
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
import { generateRequestId } from '@/lib/core/utils/request'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
const logger = createLogger('GoogleBigQueryDatasetsAPI')
export const dynamic = 'force-dynamic'
/**
* POST /api/tools/google_bigquery/datasets
*
* Fetches the list of BigQuery datasets for a given project using the caller's OAuth credential.
*
* @param request - Incoming request containing `credential`, `workflowId`, and `projectId` in the JSON body
* @returns JSON response with a `datasets` array, each entry containing `datasetReference` and optional `friendlyName`
*/
export async function POST(request: Request) {
const requestId = generateRequestId()
try {
const body = await request.json()
const { credential, workflowId, projectId } = body
if (!credential) {
logger.error('Missing credential in request')
return NextResponse.json({ error: 'Credential is required' }, { status: 400 })
}
if (!projectId) {
logger.error('Missing project ID in request')
return NextResponse.json({ error: 'Project ID is required' }, { status: 400 })
}
const authz = await authorizeCredentialUse(request as any, {
credentialId: credential,
workflowId,
})
if (!authz.ok || !authz.credentialOwnerUserId) {
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
}
const accessToken = await refreshAccessTokenIfNeeded(
credential,
authz.credentialOwnerUserId,
requestId
)
if (!accessToken) {
logger.error('Failed to get access token', {
credentialId: credential,
userId: authz.credentialOwnerUserId,
})
return NextResponse.json(
{ error: 'Could not retrieve access token', authRequired: true },
{ status: 401 }
)
}
const response = await fetch(
`https://bigquery.googleapis.com/bigquery/v2/projects/${encodeURIComponent(projectId)}/datasets?maxResults=200`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
}
)
if (!response.ok) {
const errorData = await response.json().catch(() => ({}))
logger.error('Failed to fetch BigQuery datasets', {
status: response.status,
error: errorData,
})
return NextResponse.json(
{ error: 'Failed to fetch BigQuery datasets', details: errorData },
{ status: response.status }
)
}
const data = await response.json()
const datasets = (data.datasets || []).map(
(ds: {
datasetReference: { datasetId: string; projectId: string }
friendlyName?: string
}) => ({
datasetReference: ds.datasetReference,
friendlyName: ds.friendlyName,
})
)
return NextResponse.json({ datasets })
} catch (error) {
logger.error('Error processing BigQuery datasets request:', error)
return NextResponse.json(
{ error: 'Failed to retrieve BigQuery datasets', details: (error as Error).message },
{ status: 500 }
)
}
}
@@ -0,0 +1,94 @@
import { createLogger } from '@sim/logger'
import { NextResponse } from 'next/server'
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
import { generateRequestId } from '@/lib/core/utils/request'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
const logger = createLogger('GoogleBigQueryTablesAPI')
export const dynamic = 'force-dynamic'
export async function POST(request: Request) {
const requestId = generateRequestId()
try {
const body = await request.json()
const { credential, workflowId, projectId, datasetId } = body
if (!credential) {
logger.error('Missing credential in request')
return NextResponse.json({ error: 'Credential is required' }, { status: 400 })
}
if (!projectId) {
logger.error('Missing project ID in request')
return NextResponse.json({ error: 'Project ID is required' }, { status: 400 })
}
if (!datasetId) {
logger.error('Missing dataset ID in request')
return NextResponse.json({ error: 'Dataset ID is required' }, { status: 400 })
}
const authz = await authorizeCredentialUse(request as any, {
credentialId: credential,
workflowId,
})
if (!authz.ok || !authz.credentialOwnerUserId) {
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
}
const accessToken = await refreshAccessTokenIfNeeded(
credential,
authz.credentialOwnerUserId,
requestId
)
if (!accessToken) {
logger.error('Failed to get access token', {
credentialId: credential,
userId: authz.credentialOwnerUserId,
})
return NextResponse.json(
{ error: 'Could not retrieve access token', authRequired: true },
{ status: 401 }
)
}
const response = await fetch(
`https://bigquery.googleapis.com/bigquery/v2/projects/${encodeURIComponent(projectId)}/datasets/${encodeURIComponent(datasetId)}/tables?maxResults=200`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
}
)
if (!response.ok) {
const errorData = await response.json().catch(() => ({}))
logger.error('Failed to fetch BigQuery tables', {
status: response.status,
error: errorData,
})
return NextResponse.json(
{ error: 'Failed to fetch BigQuery tables', details: errorData },
{ status: response.status }
)
}
const data = await response.json()
const tables = (data.tables || []).map(
(t: { tableReference: { tableId: string }; friendlyName?: string }) => ({
tableReference: t.tableReference,
friendlyName: t.friendlyName,
})
)
return NextResponse.json({ tables })
} catch (error) {
logger.error('Error processing BigQuery tables request:', error)
return NextResponse.json(
{ error: 'Failed to retrieve BigQuery tables', details: (error as Error).message },
{ status: 500 }
)
}
}
@@ -0,0 +1,79 @@
import { createLogger } from '@sim/logger'
import { NextResponse } from 'next/server'
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
import { generateRequestId } from '@/lib/core/utils/request'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
const logger = createLogger('GoogleTasksTaskListsAPI')
export const dynamic = 'force-dynamic'
export async function POST(request: Request) {
const requestId = generateRequestId()
try {
const body = await request.json()
const { credential, workflowId } = body
if (!credential) {
logger.error('Missing credential in request')
return NextResponse.json({ error: 'Credential is required' }, { status: 400 })
}
const authz = await authorizeCredentialUse(request as any, {
credentialId: credential,
workflowId,
})
if (!authz.ok || !authz.credentialOwnerUserId) {
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
}
const accessToken = await refreshAccessTokenIfNeeded(
credential,
authz.credentialOwnerUserId,
requestId
)
if (!accessToken) {
logger.error('Failed to get access token', {
credentialId: credential,
userId: authz.credentialOwnerUserId,
})
return NextResponse.json(
{ error: 'Could not retrieve access token', authRequired: true },
{ status: 401 }
)
}
const response = await fetch('https://tasks.googleapis.com/tasks/v1/users/@me/lists', {
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
})
if (!response.ok) {
const errorData = await response.json().catch(() => ({}))
logger.error('Failed to fetch Google Tasks task lists', {
status: response.status,
error: errorData,
})
return NextResponse.json(
{ error: 'Failed to fetch Google Tasks task lists', details: errorData },
{ status: response.status }
)
}
const data = await response.json()
const taskLists = (data.items || []).map((list: { id: string; title: string }) => ({
id: list.id,
title: list.title,
}))
return NextResponse.json({ taskLists })
} catch (error) {
logger.error('Error processing Google Tasks task lists request:', error)
return NextResponse.json(
{ error: 'Failed to retrieve Google Tasks task lists', details: (error as Error).message },
{ status: 500 }
)
}
}
@@ -0,0 +1,103 @@
import { createLogger } from '@sim/logger'
import { NextResponse } from 'next/server'
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
import { generateRequestId } from '@/lib/core/utils/request'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
const logger = createLogger('JsmSelectorRequestTypesAPI')
export const dynamic = 'force-dynamic'
export async function POST(request: Request) {
const requestId = generateRequestId()
try {
const body = await request.json()
const { credential, workflowId, domain, serviceDeskId } = body
if (!credential) {
logger.error('Missing credential in request')
return NextResponse.json({ error: 'Credential is required' }, { status: 400 })
}
if (!domain) {
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
}
if (!serviceDeskId) {
return NextResponse.json({ error: 'Service Desk ID is required' }, { status: 400 })
}
const serviceDeskIdValidation = validateAlphanumericId(serviceDeskId, 'serviceDeskId')
if (!serviceDeskIdValidation.isValid) {
return NextResponse.json({ error: serviceDeskIdValidation.error }, { status: 400 })
}
const authz = await authorizeCredentialUse(request as any, {
credentialId: credential,
workflowId,
})
if (!authz.ok || !authz.credentialOwnerUserId) {
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
}
const accessToken = await refreshAccessTokenIfNeeded(
credential,
authz.credentialOwnerUserId,
requestId
)
if (!accessToken) {
logger.error('Failed to get access token', {
credentialId: credential,
userId: authz.credentialOwnerUserId,
})
return NextResponse.json(
{ error: 'Could not retrieve access token', authRequired: true },
{ status: 401 }
)
}
const cloudId = await getJiraCloudId(domain, accessToken)
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
if (!cloudIdValidation.isValid) {
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
}
const baseUrl = getJsmApiBaseUrl(cloudIdValidation.sanitized!)
const url = `${baseUrl}/servicedesk/${serviceDeskIdValidation.sanitized}/requesttype?limit=100`
const response = await fetch(url, {
method: 'GET',
headers: getJsmHeaders(accessToken),
})
if (!response.ok) {
const errorText = await response.text()
logger.error('JSM API error:', {
status: response.status,
statusText: response.statusText,
error: errorText,
})
return NextResponse.json(
{ error: `JSM API error: ${response.status} ${response.statusText}` },
{ status: response.status }
)
}
const data = await response.json()
const requestTypes = (data.values || []).map((rt: { id: string; name: string }) => ({
id: rt.id,
name: rt.name,
}))
return NextResponse.json({ requestTypes })
} catch (error) {
logger.error('Error listing JSM request types:', error)
return NextResponse.json(
{ error: (error as Error).message || 'Internal server error' },
{ status: 500 }
)
}
}
@@ -0,0 +1,94 @@
import { createLogger } from '@sim/logger'
import { NextResponse } from 'next/server'
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
import { validateJiraCloudId } from '@/lib/core/security/input-validation'
import { generateRequestId } from '@/lib/core/utils/request'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
const logger = createLogger('JsmSelectorServiceDesksAPI')
export const dynamic = 'force-dynamic'
export async function POST(request: Request) {
const requestId = generateRequestId()
try {
const body = await request.json()
const { credential, workflowId, domain } = body
if (!credential) {
logger.error('Missing credential in request')
return NextResponse.json({ error: 'Credential is required' }, { status: 400 })
}
if (!domain) {
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
}
const authz = await authorizeCredentialUse(request as any, {
credentialId: credential,
workflowId,
})
if (!authz.ok || !authz.credentialOwnerUserId) {
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
}
const accessToken = await refreshAccessTokenIfNeeded(
credential,
authz.credentialOwnerUserId,
requestId
)
if (!accessToken) {
logger.error('Failed to get access token', {
credentialId: credential,
userId: authz.credentialOwnerUserId,
})
return NextResponse.json(
{ error: 'Could not retrieve access token', authRequired: true },
{ status: 401 }
)
}
const cloudId = await getJiraCloudId(domain, accessToken)
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
if (!cloudIdValidation.isValid) {
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
}
const baseUrl = getJsmApiBaseUrl(cloudIdValidation.sanitized!)
const url = `${baseUrl}/servicedesk?limit=100`
const response = await fetch(url, {
method: 'GET',
headers: getJsmHeaders(accessToken),
})
if (!response.ok) {
const errorText = await response.text()
logger.error('JSM API error:', {
status: response.status,
statusText: response.statusText,
error: errorText,
})
return NextResponse.json(
{ error: `JSM API error: ${response.status} ${response.statusText}` },
{ status: response.status }
)
}
const data = await response.json()
const serviceDesks = (data.values || []).map((sd: { id: string; projectName: string }) => ({
id: sd.id,
name: sd.projectName,
}))
return NextResponse.json({ serviceDesks })
} catch (error) {
logger.error('Error listing JSM service desks:', error)
return NextResponse.json(
{ error: (error as Error).message || 'Internal server error' },
{ status: 500 }
)
}
}
@@ -0,0 +1,72 @@
import { createLogger } from '@sim/logger'
import { NextResponse } from 'next/server'
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
import { generateRequestId } from '@/lib/core/utils/request'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
const logger = createLogger('MicrosoftPlannerPlansAPI')
export const dynamic = 'force-dynamic'
export async function POST(request: Request) {
const requestId = generateRequestId()
try {
const body = await request.json()
const { credential, workflowId } = body
if (!credential) {
logger.error(`[${requestId}] Missing credential in request`)
return NextResponse.json({ error: 'Credential is required' }, { status: 400 })
}
const authz = await authorizeCredentialUse(request as any, {
credentialId: credential,
workflowId,
})
if (!authz.ok || !authz.credentialOwnerUserId) {
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
}
const accessToken = await refreshAccessTokenIfNeeded(
credential,
authz.credentialOwnerUserId,
requestId
)
if (!accessToken) {
logger.error(`[${requestId}] Failed to obtain valid access token`)
return NextResponse.json(
{ error: 'Failed to obtain valid access token', authRequired: true },
{ status: 401 }
)
}
const response = await fetch('https://graph.microsoft.com/v1.0/me/planner/plans', {
headers: {
Authorization: `Bearer ${accessToken}`,
},
})
if (!response.ok) {
const errorText = await response.text()
logger.error(`[${requestId}] Microsoft Graph API error:`, errorText)
return NextResponse.json(
{ error: 'Failed to fetch plans from Microsoft Graph' },
{ status: response.status }
)
}
const data = await response.json()
const plans = data.value || []
const filteredPlans = plans.map((plan: { id: string; title: string }) => ({
id: plan.id,
title: plan.title,
}))
return NextResponse.json({ plans: filteredPlans })
} catch (error) {
logger.error(`[${requestId}] Error fetching Microsoft Planner plans:`, error)
return NextResponse.json({ error: 'Failed to fetch plans' }, { status: 500 })
}
}
@@ -1,38 +1,29 @@
import { randomUUID } from 'crypto'
import { db } from '@sim/db'
import { account } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { NextResponse } from 'next/server'
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
import { validateMicrosoftGraphId } from '@/lib/core/security/input-validation'
import { refreshAccessTokenIfNeeded, resolveOAuthAccountId } from '@/app/api/auth/oauth/utils'
import { generateRequestId } from '@/lib/core/utils/request'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
import type { PlannerTask } from '@/tools/microsoft_planner/types'
const logger = createLogger('MicrosoftPlannerTasksAPI')
export async function GET(request: NextRequest) {
const requestId = randomUUID().slice(0, 8)
export const dynamic = 'force-dynamic'
export async function POST(request: Request) {
const requestId = generateRequestId()
try {
const session = await getSession()
const body = await request.json()
const { credential, workflowId, planId } = body
if (!session?.user?.id) {
logger.warn(`[${requestId}] Unauthenticated request rejected`)
return NextResponse.json({ error: 'User not authenticated' }, { status: 401 })
}
const { searchParams } = new URL(request.url)
const credentialId = searchParams.get('credentialId')
const planId = searchParams.get('planId')
if (!credentialId) {
logger.error(`[${requestId}] Missing credentialId parameter`)
return NextResponse.json({ error: 'Credential ID is required' }, { status: 400 })
if (!credential) {
logger.error(`[${requestId}] Missing credential in request`)
return NextResponse.json({ error: 'Credential is required' }, { status: 400 })
}
if (!planId) {
logger.error(`[${requestId}] Missing planId parameter`)
logger.error(`[${requestId}] Missing planId in request`)
return NextResponse.json({ error: 'Plan ID is required' }, { status: 400 })
}
@@ -42,52 +33,35 @@ export async function GET(request: NextRequest) {
return NextResponse.json({ error: planIdValidation.error }, { status: 400 })
}
const resolved = await resolveOAuthAccountId(credentialId)
if (!resolved) {
return NextResponse.json({ error: 'Credential not found' }, { status: 404 })
const authz = await authorizeCredentialUse(request as any, {
credentialId: credential,
workflowId,
})
if (!authz.ok || !authz.credentialOwnerUserId) {
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
}
if (resolved.workspaceId) {
const { getUserEntityPermissions } = await import('@/lib/workspaces/permissions/utils')
const perm = await getUserEntityPermissions(
session.user.id,
'workspace',
resolved.workspaceId
)
if (perm === null) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
}
}
const credentials = await db
.select()
.from(account)
.where(eq(account.id, resolved.accountId))
.limit(1)
if (!credentials.length) {
logger.warn(`[${requestId}] Credential not found`, { credentialId })
return NextResponse.json({ error: 'Credential not found' }, { status: 404 })
}
const accountRow = credentials[0]
const accessToken = await refreshAccessTokenIfNeeded(
resolved.accountId,
accountRow.userId,
credential,
authz.credentialOwnerUserId,
requestId
)
if (!accessToken) {
logger.error(`[${requestId}] Failed to obtain valid access token`)
return NextResponse.json({ error: 'Failed to obtain valid access token' }, { status: 401 })
return NextResponse.json(
{ error: 'Failed to obtain valid access token', authRequired: true },
{ status: 401 }
)
}
const response = await fetch(`https://graph.microsoft.com/v1.0/planner/plans/${planId}/tasks`, {
headers: {
Authorization: `Bearer ${accessToken}`,
},
})
const response = await fetch(
`https://graph.microsoft.com/v1.0/planner/plans/${planIdValidation.sanitized}/tasks`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
},
}
)
if (!response.ok) {
const errorText = await response.text()
@@ -0,0 +1,86 @@
import { createLogger } from '@sim/logger'
import { NextResponse } from 'next/server'
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
import { generateRequestId } from '@/lib/core/utils/request'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
import { extractTitleFromItem } from '@/tools/notion/utils'
const logger = createLogger('NotionDatabasesAPI')
export const dynamic = 'force-dynamic'
export async function POST(request: Request) {
const requestId = generateRequestId()
try {
const body = await request.json()
const { credential, workflowId } = body
if (!credential) {
logger.error('Missing credential in request')
return NextResponse.json({ error: 'Credential is required' }, { status: 400 })
}
const authz = await authorizeCredentialUse(request as any, {
credentialId: credential,
workflowId,
})
if (!authz.ok || !authz.credentialOwnerUserId) {
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
}
const accessToken = await refreshAccessTokenIfNeeded(
credential,
authz.credentialOwnerUserId,
requestId
)
if (!accessToken) {
logger.error('Failed to get access token', {
credentialId: credential,
userId: authz.credentialOwnerUserId,
})
return NextResponse.json(
{ error: 'Could not retrieve access token', authRequired: true },
{ status: 401 }
)
}
const response = await fetch('https://api.notion.com/v1/search', {
method: 'POST',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
'Notion-Version': '2022-06-28',
},
body: JSON.stringify({
filter: { value: 'database', property: 'object' },
page_size: 100,
}),
})
if (!response.ok) {
const errorData = await response.json().catch(() => ({}))
logger.error('Failed to fetch Notion databases', {
status: response.status,
error: errorData,
})
return NextResponse.json(
{ error: 'Failed to fetch Notion databases', details: errorData },
{ status: response.status }
)
}
const data = await response.json()
const databases = (data.results || []).map((db: Record<string, unknown>) => ({
id: db.id as string,
name: extractTitleFromItem(db),
}))
return NextResponse.json({ databases })
} catch (error) {
logger.error('Error processing Notion databases request:', error)
return NextResponse.json(
{ error: 'Failed to retrieve Notion databases', details: (error as Error).message },
{ status: 500 }
)
}
}
@@ -0,0 +1,86 @@
import { createLogger } from '@sim/logger'
import { NextResponse } from 'next/server'
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
import { generateRequestId } from '@/lib/core/utils/request'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
import { extractTitleFromItem } from '@/tools/notion/utils'
const logger = createLogger('NotionPagesAPI')
export const dynamic = 'force-dynamic'
export async function POST(request: Request) {
const requestId = generateRequestId()
try {
const body = await request.json()
const { credential, workflowId } = body
if (!credential) {
logger.error('Missing credential in request')
return NextResponse.json({ error: 'Credential is required' }, { status: 400 })
}
const authz = await authorizeCredentialUse(request as any, {
credentialId: credential,
workflowId,
})
if (!authz.ok || !authz.credentialOwnerUserId) {
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
}
const accessToken = await refreshAccessTokenIfNeeded(
credential,
authz.credentialOwnerUserId,
requestId
)
if (!accessToken) {
logger.error('Failed to get access token', {
credentialId: credential,
userId: authz.credentialOwnerUserId,
})
return NextResponse.json(
{ error: 'Could not retrieve access token', authRequired: true },
{ status: 401 }
)
}
const response = await fetch('https://api.notion.com/v1/search', {
method: 'POST',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
'Notion-Version': '2022-06-28',
},
body: JSON.stringify({
filter: { value: 'page', property: 'object' },
page_size: 100,
}),
})
if (!response.ok) {
const errorData = await response.json().catch(() => ({}))
logger.error('Failed to fetch Notion pages', {
status: response.status,
error: errorData,
})
return NextResponse.json(
{ error: 'Failed to fetch Notion pages', details: errorData },
{ status: response.status }
)
}
const data = await response.json()
const pages = (data.results || []).map((page: Record<string, unknown>) => ({
id: page.id as string,
name: extractTitleFromItem(page),
}))
return NextResponse.json({ pages })
} catch (error) {
logger.error('Error processing Notion pages request:', error)
return NextResponse.json(
{ error: 'Failed to retrieve Notion pages', details: (error as Error).message },
{ status: 500 }
)
}
}
@@ -0,0 +1,79 @@
import { createLogger } from '@sim/logger'
import { NextResponse } from 'next/server'
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
import { generateRequestId } from '@/lib/core/utils/request'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
const logger = createLogger('PipedrivePipelinesAPI')
export const dynamic = 'force-dynamic'
export async function POST(request: Request) {
const requestId = generateRequestId()
try {
const body = await request.json()
const { credential, workflowId } = body
if (!credential) {
logger.error('Missing credential in request')
return NextResponse.json({ error: 'Credential is required' }, { status: 400 })
}
const authz = await authorizeCredentialUse(request as any, {
credentialId: credential,
workflowId,
})
if (!authz.ok || !authz.credentialOwnerUserId) {
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
}
const accessToken = await refreshAccessTokenIfNeeded(
credential,
authz.credentialOwnerUserId,
requestId
)
if (!accessToken) {
logger.error('Failed to get access token', {
credentialId: credential,
userId: authz.credentialOwnerUserId,
})
return NextResponse.json(
{ error: 'Could not retrieve access token', authRequired: true },
{ status: 401 }
)
}
const response = await fetch('https://api.pipedrive.com/v1/pipelines', {
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
})
if (!response.ok) {
const errorData = await response.json().catch(() => ({}))
logger.error('Failed to fetch Pipedrive pipelines', {
status: response.status,
error: errorData,
})
return NextResponse.json(
{ error: 'Failed to fetch Pipedrive pipelines', details: errorData },
{ status: response.status }
)
}
const data = await response.json()
const pipelines = (data.data || []).map((pipeline: { id: number; name: string }) => ({
id: String(pipeline.id),
name: pipeline.name,
}))
return NextResponse.json({ pipelines })
} catch (error) {
logger.error('Error processing Pipedrive pipelines request:', error)
return NextResponse.json(
{ error: 'Failed to retrieve Pipedrive pipelines', details: (error as Error).message },
{ status: 500 }
)
}
}
@@ -0,0 +1,91 @@
import { createLogger } from '@sim/logger'
import { NextResponse } from 'next/server'
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
import { validateSharePointSiteId } from '@/lib/core/security/input-validation'
import { generateRequestId } from '@/lib/core/utils/request'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
export const dynamic = 'force-dynamic'
const logger = createLogger('SharePointListsAPI')
interface SharePointList {
id: string
displayName: string
description?: string
webUrl?: string
list?: {
hidden?: boolean
}
}
export async function POST(request: Request) {
const requestId = generateRequestId()
try {
const body = await request.json()
const { credential, workflowId, siteId } = body
if (!credential) {
logger.error(`[${requestId}] Missing credential in request`)
return NextResponse.json({ error: 'Credential is required' }, { status: 400 })
}
const siteIdValidation = validateSharePointSiteId(siteId)
if (!siteIdValidation.isValid) {
logger.error(`[${requestId}] Invalid siteId: ${siteIdValidation.error}`)
return NextResponse.json({ error: siteIdValidation.error }, { status: 400 })
}
const authz = await authorizeCredentialUse(request as any, {
credentialId: credential,
workflowId,
})
if (!authz.ok || !authz.credentialOwnerUserId) {
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
}
const accessToken = await refreshAccessTokenIfNeeded(
credential,
authz.credentialOwnerUserId,
requestId
)
if (!accessToken) {
logger.error(`[${requestId}] Failed to obtain valid access token`)
return NextResponse.json(
{ error: 'Failed to obtain valid access token', authRequired: true },
{ status: 401 }
)
}
const url = `https://graph.microsoft.com/v1.0/sites/${siteIdValidation.sanitized}/lists?$select=id,displayName,description,webUrl&$expand=list($select=hidden)&$top=100`
const response = await fetch(url, {
headers: {
Authorization: `Bearer ${accessToken}`,
},
})
if (!response.ok) {
const errorData = await response.json().catch(() => ({ error: { message: 'Unknown error' } }))
return NextResponse.json(
{ error: errorData.error?.message || 'Failed to fetch lists from SharePoint' },
{ status: response.status }
)
}
const data = await response.json()
const lists = (data.value || [])
.filter((list: SharePointList) => list.list?.hidden !== true)
.map((list: SharePointList) => ({
id: list.id,
displayName: list.displayName,
}))
logger.info(`[${requestId}] Successfully fetched ${lists.length} SharePoint lists`)
return NextResponse.json({ lists }, { status: 200 })
} catch (error) {
logger.error(`[${requestId}] Error fetching lists from SharePoint`, error)
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
}
}
@@ -1,79 +1,45 @@
import { randomUUID } from 'crypto'
import { db } from '@sim/db'
import { account } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
import { refreshAccessTokenIfNeeded, resolveOAuthAccountId } from '@/app/api/auth/oauth/utils'
import { NextResponse } from 'next/server'
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
import { generateRequestId } from '@/lib/core/utils/request'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
import type { SharepointSite } from '@/tools/sharepoint/types'
export const dynamic = 'force-dynamic'
const logger = createLogger('SharePointSitesAPI')
/**
* Get SharePoint sites from Microsoft Graph API
*/
export async function GET(request: NextRequest) {
const requestId = randomUUID().slice(0, 8)
export async function POST(request: Request) {
const requestId = generateRequestId()
try {
const session = await getSession()
if (!session?.user?.id) {
return NextResponse.json({ error: 'User not authenticated' }, { status: 401 })
const body = await request.json()
const { credential, workflowId, query } = body
if (!credential) {
logger.error(`[${requestId}] Missing credential in request`)
return NextResponse.json({ error: 'Credential is required' }, { status: 400 })
}
const { searchParams } = new URL(request.url)
const credentialId = searchParams.get('credentialId')
const query = searchParams.get('query') || ''
if (!credentialId) {
return NextResponse.json({ error: 'Credential ID is required' }, { status: 400 })
const authz = await authorizeCredentialUse(request as any, {
credentialId: credential,
workflowId,
})
if (!authz.ok || !authz.credentialOwnerUserId) {
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
}
const credentialIdValidation = validateAlphanumericId(credentialId, 'credentialId', 255)
if (!credentialIdValidation.isValid) {
logger.warn(`[${requestId}] Invalid credential ID`, { error: credentialIdValidation.error })
return NextResponse.json({ error: credentialIdValidation.error }, { status: 400 })
}
const resolved = await resolveOAuthAccountId(credentialId)
if (!resolved) {
return NextResponse.json({ error: 'Credential not found' }, { status: 404 })
}
if (resolved.workspaceId) {
const { getUserEntityPermissions } = await import('@/lib/workspaces/permissions/utils')
const perm = await getUserEntityPermissions(
session.user.id,
'workspace',
resolved.workspaceId
)
if (perm === null) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
}
}
const credentials = await db
.select()
.from(account)
.where(eq(account.id, resolved.accountId))
.limit(1)
if (!credentials.length) {
return NextResponse.json({ error: 'Credential not found' }, { status: 404 })
}
const accountRow = credentials[0]
const accessToken = await refreshAccessTokenIfNeeded(
resolved.accountId,
accountRow.userId,
credential,
authz.credentialOwnerUserId,
requestId
)
if (!accessToken) {
return NextResponse.json({ error: 'Failed to obtain valid access token' }, { status: 401 })
logger.error(`[${requestId}] Failed to obtain valid access token`)
return NextResponse.json(
{ error: 'Failed to obtain valid access token', authRequired: true },
{ status: 401 }
)
}
const searchQuery = query || '*'
@@ -0,0 +1,87 @@
import { createLogger } from '@sim/logger'
import { NextResponse } from 'next/server'
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
import { generateRequestId } from '@/lib/core/utils/request'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
const logger = createLogger('TrelloBoardsAPI')
export const dynamic = 'force-dynamic'
export async function POST(request: Request) {
const requestId = generateRequestId()
try {
const apiKey = process.env.TRELLO_API_KEY
if (!apiKey) {
logger.error('Trello API key not configured')
return NextResponse.json({ error: 'Trello API key not configured' }, { status: 500 })
}
const body = await request.json()
const { credential, workflowId } = body
if (!credential) {
logger.error('Missing credential in request')
return NextResponse.json({ error: 'Credential is required' }, { status: 400 })
}
const authz = await authorizeCredentialUse(request as any, {
credentialId: credential,
workflowId,
})
if (!authz.ok || !authz.credentialOwnerUserId) {
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
}
const accessToken = await refreshAccessTokenIfNeeded(
credential,
authz.credentialOwnerUserId,
requestId
)
if (!accessToken) {
logger.error('Failed to get access token', {
credentialId: credential,
userId: authz.credentialOwnerUserId,
})
return NextResponse.json(
{ error: 'Could not retrieve access token', authRequired: true },
{ status: 401 }
)
}
const response = await fetch(
`https://api.trello.com/1/members/me/boards?key=${apiKey}&token=${accessToken}&fields=id,name,closed`,
{
headers: {
Accept: 'application/json',
},
}
)
if (!response.ok) {
const errorData = await response.json().catch(() => ({}))
logger.error('Failed to fetch Trello boards', {
status: response.status,
error: errorData,
})
return NextResponse.json(
{ error: 'Failed to fetch Trello boards', details: errorData },
{ status: response.status }
)
}
const data = await response.json()
const boards = (data || []).map((board: { id: string; name: string; closed: boolean }) => ({
id: board.id,
name: board.name,
closed: board.closed,
}))
return NextResponse.json({ boards })
} catch (error) {
logger.error('Error processing Trello boards request:', error)
return NextResponse.json(
{ error: 'Failed to retrieve Trello boards', details: (error as Error).message },
{ status: 500 }
)
}
}
@@ -0,0 +1,82 @@
import { createLogger } from '@sim/logger'
import { NextResponse } from 'next/server'
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
import { generateRequestId } from '@/lib/core/utils/request'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
const logger = createLogger('ZoomMeetingsAPI')
export const dynamic = 'force-dynamic'
export async function POST(request: Request) {
const requestId = generateRequestId()
try {
const body = await request.json()
const { credential, workflowId } = body
if (!credential) {
logger.error('Missing credential in request')
return NextResponse.json({ error: 'Credential is required' }, { status: 400 })
}
const authz = await authorizeCredentialUse(request as any, {
credentialId: credential,
workflowId,
})
if (!authz.ok || !authz.credentialOwnerUserId) {
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
}
const accessToken = await refreshAccessTokenIfNeeded(
credential,
authz.credentialOwnerUserId,
requestId
)
if (!accessToken) {
logger.error('Failed to get access token', {
credentialId: credential,
userId: authz.credentialOwnerUserId,
})
return NextResponse.json(
{ error: 'Could not retrieve access token', authRequired: true },
{ status: 401 }
)
}
const response = await fetch(
'https://api.zoom.us/v2/users/me/meetings?page_size=300&type=scheduled',
{
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
}
)
if (!response.ok) {
const errorData = await response.json().catch(() => ({}))
logger.error('Failed to fetch Zoom meetings', {
status: response.status,
error: errorData,
})
return NextResponse.json(
{ error: 'Failed to fetch Zoom meetings', details: errorData },
{ status: response.status }
)
}
const data = await response.json()
const meetings = (data.meetings || []).map((meeting: { id: number; topic: string }) => ({
id: String(meeting.id),
name: meeting.topic,
}))
return NextResponse.json({ meetings })
} catch (error) {
logger.error('Error processing Zoom meetings request:', error)
return NextResponse.json(
{ error: 'Failed to retrieve Zoom meetings', details: (error as Error).message },
{ status: 500 }
)
}
}
@@ -3,6 +3,7 @@
import { useMemo } from 'react'
import { useParams } from 'next/navigation'
import type { SubBlockConfig } from '@/blocks/types'
import { isEnvVarReference, isReference } from '@/executor/constants'
import type { SelectorContext, SelectorKey } from '@/hooks/selectors/types'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { useDependsOnGate } from './use-depends-on-gate'
@@ -45,6 +46,7 @@ export function useSelectorSetup(
if (value === null || value === undefined) continue
const strValue = String(value)
if (!strValue) continue
if (isReference(strValue) || isEnvVarReference(strValue)) continue
const canonicalParamId = canonicalIndex.canonicalIdBySubBlockId[depKey] ?? depKey
@@ -78,4 +80,7 @@ const CONTEXT_FIELD_SET: Record<string, true> = {
collectionId: true,
spreadsheetId: true,
fileId: true,
baseId: true,
datasetId: true,
serviceDeskId: true,
}
@@ -549,10 +549,30 @@ const SubBlockRow = memo(function SubBlockRow({
return typeof option === 'string' ? option : option.label
}, [subBlock, rawValue])
const domainValue = getStringValue('domain')
const teamIdValue = getStringValue('teamId')
const projectIdValue = getStringValue('projectId')
const planIdValue = getStringValue('planId')
const resolveContextValue = useCallback(
(key: string): string | undefined => {
const resolved = resolveDependencyValue(
key,
rawValues,
canonicalIndex || buildCanonicalIndex([]),
canonicalModeOverrides
)
return typeof resolved === 'string' && resolved.length > 0 ? resolved : undefined
},
[rawValues, canonicalIndex, canonicalModeOverrides]
)
const domainValue = resolveContextValue('domain')
const teamIdValue = resolveContextValue('teamId')
const projectIdValue = resolveContextValue('projectId')
const planIdValue = resolveContextValue('planId')
const baseIdValue = resolveContextValue('baseId')
const datasetIdValue = resolveContextValue('datasetId')
const serviceDeskIdValue = resolveContextValue('serviceDeskId')
const siteIdValue = resolveContextValue('siteId')
const collectionIdValue = resolveContextValue('collectionId')
const spreadsheetIdValue = resolveContextValue('spreadsheetId')
const fileIdValue = resolveContextValue('fileId')
const { displayName: selectorDisplayName } = useSelectorDisplayName({
subBlock,
@@ -564,6 +584,13 @@ const SubBlockRow = memo(function SubBlockRow({
teamId: teamIdValue,
projectId: projectIdValue,
planId: planIdValue,
baseId: baseIdValue,
datasetId: datasetIdValue,
serviceDeskId: serviceDeskIdValue,
siteId: siteIdValue,
collectionId: collectionIdValue,
spreadsheetId: spreadsheetIdValue,
fileId: fileIdValue,
})
const { knowledgeBase: kbForDisplayName } = useKnowledgeBase(
+10 -3
View File
@@ -667,15 +667,18 @@ describe.concurrent('Blocks Module', () => {
const errors: string[] = []
for (const block of blocks) {
const allSubBlockIds = new Set(block.subBlocks.map((sb) => sb.id))
// Exclude trigger-mode subBlocks — they operate in a separate rendering context
// and their IDs don't participate in canonical param resolution
const nonTriggerSubBlocks = block.subBlocks.filter((sb) => sb.mode !== 'trigger')
const allSubBlockIds = new Set(nonTriggerSubBlocks.map((sb) => sb.id))
const canonicalParamIds = new Set(
block.subBlocks.filter((sb) => sb.canonicalParamId).map((sb) => sb.canonicalParamId)
nonTriggerSubBlocks.filter((sb) => sb.canonicalParamId).map((sb) => sb.canonicalParamId)
)
for (const canonicalId of canonicalParamIds) {
if (allSubBlockIds.has(canonicalId!)) {
// Check if the matching subBlock also has a canonicalParamId pointing to itself
const matchingSubBlock = block.subBlocks.find(
const matchingSubBlock = nonTriggerSubBlocks.find(
(sb) => sb.id === canonicalId && !sb.canonicalParamId
)
if (matchingSubBlock) {
@@ -857,6 +860,10 @@ describe.concurrent('Blocks Module', () => {
if (typeof subBlock.condition === 'function') {
continue
}
// Skip trigger-mode subBlocks — they operate in a separate rendering context
if (subBlock.mode === 'trigger') {
continue
}
const conditionKey = serializeCondition(subBlock.condition)
if (!canonicalByCondition.has(subBlock.canonicalParamId)) {
canonicalByCondition.set(subBlock.canonicalParamId, new Set())
+32 -2
View File
@@ -57,21 +57,51 @@ export const AirtableBlock: BlockConfig<AirtableResponse> = {
placeholder: 'Enter credential ID',
required: true,
},
{
id: 'baseSelector',
title: 'Base',
type: 'project-selector',
canonicalParamId: 'baseId',
serviceId: 'airtable',
selectorKey: 'airtable.bases',
selectorAllowSearch: false,
placeholder: 'Select Airtable base',
dependsOn: ['credential'],
mode: 'basic',
condition: { field: 'operation', value: 'listBases', not: true },
required: { field: 'operation', value: 'listBases', not: true },
},
{
id: 'baseId',
title: 'Base ID',
type: 'short-input',
canonicalParamId: 'baseId',
placeholder: 'Enter your base ID (e.g., appXXXXXXXXXXXXXX)',
dependsOn: ['credential'],
mode: 'advanced',
condition: { field: 'operation', value: 'listBases', not: true },
required: { field: 'operation', value: 'listBases', not: true },
},
{
id: 'tableSelector',
title: 'Table',
type: 'file-selector',
canonicalParamId: 'tableId',
serviceId: 'airtable',
selectorKey: 'airtable.tables',
selectorAllowSearch: false,
placeholder: 'Select Airtable table',
dependsOn: ['credential', 'baseSelector'],
mode: 'basic',
condition: { field: 'operation', value: ['listBases', 'listTables'], not: true },
required: { field: 'operation', value: ['listBases', 'listTables'], not: true },
},
{
id: 'tableId',
title: 'Table ID',
type: 'short-input',
canonicalParamId: 'tableId',
placeholder: 'Enter table ID (e.g., tblXXXXXXXXXXXXXX)',
dependsOn: ['credential', 'baseId'],
mode: 'advanced',
condition: { field: 'operation', value: ['listBases', 'listTables'], not: true },
required: { field: 'operation', value: ['listBases', 'listTables'], not: true },
},
+37
View File
@@ -48,12 +48,31 @@ export const AsanaBlock: BlockConfig<AsanaResponse> = {
placeholder: 'Enter credential ID',
required: true,
},
{
id: 'workspaceSelector',
title: 'Workspace',
type: 'project-selector',
canonicalParamId: 'workspace',
serviceId: 'asana',
selectorKey: 'asana.workspaces',
selectorAllowSearch: false,
placeholder: 'Select Asana workspace',
dependsOn: ['credential'],
mode: 'basic',
condition: {
field: 'operation',
value: ['create_task', 'get_projects', 'search_tasks'],
},
required: true,
},
{
id: 'workspace',
title: 'Workspace GID',
type: 'short-input',
canonicalParamId: 'workspace',
required: true,
placeholder: 'Enter Asana workspace GID',
mode: 'advanced',
condition: {
field: 'operation',
value: ['create_task', 'get_projects', 'search_tasks'],
@@ -81,11 +100,29 @@ export const AsanaBlock: BlockConfig<AsanaResponse> = {
value: ['update_task', 'add_comment'],
},
},
{
id: 'getTasksWorkspaceSelector',
title: 'Workspace',
type: 'project-selector',
canonicalParamId: 'getTasks_workspace',
serviceId: 'asana',
selectorKey: 'asana.workspaces',
selectorAllowSearch: false,
placeholder: 'Select Asana workspace',
dependsOn: ['credential'],
mode: 'basic',
condition: {
field: 'operation',
value: ['get_task'],
},
},
{
id: 'getTasks_workspace',
title: 'Workspace GID',
type: 'short-input',
canonicalParamId: 'getTasks_workspace',
placeholder: 'Enter workspace GID',
mode: 'advanced',
condition: {
field: 'operation',
value: ['get_task'],
+74
View File
@@ -86,11 +86,47 @@ export const AttioBlock: BlockConfig<AttioResponse> = {
},
// Record fields
{
id: 'objectTypeSelector',
title: 'Object Type',
type: 'project-selector',
canonicalParamId: 'objectType',
serviceId: 'attio',
selectorKey: 'attio.objects',
selectorAllowSearch: false,
placeholder: 'Select object type',
dependsOn: ['credential'],
mode: 'basic',
condition: {
field: 'operation',
value: [
'list_records',
'get_record',
'create_record',
'update_record',
'delete_record',
'assert_record',
],
},
required: {
field: 'operation',
value: [
'list_records',
'get_record',
'create_record',
'update_record',
'delete_record',
'assert_record',
],
},
},
{
id: 'objectType',
title: 'Object Type',
type: 'short-input',
canonicalParamId: 'objectType',
placeholder: 'e.g. people, companies',
mode: 'advanced',
condition: {
field: 'operation',
value: [
@@ -524,11 +560,49 @@ Return ONLY the JSON array. No explanations, no markdown, no extra text.
},
// List fields
{
id: 'listSelector',
title: 'List',
type: 'project-selector',
canonicalParamId: 'listIdOrSlug',
serviceId: 'attio',
selectorKey: 'attio.lists',
selectorAllowSearch: false,
placeholder: 'Select Attio list',
dependsOn: ['credential'],
mode: 'basic',
condition: {
field: 'operation',
value: [
'get_list',
'update_list',
'query_list_entries',
'get_list_entry',
'create_list_entry',
'update_list_entry',
'delete_list_entry',
],
},
required: {
field: 'operation',
value: [
'get_list',
'update_list',
'query_list_entries',
'get_list_entry',
'create_list_entry',
'update_list_entry',
'delete_list_entry',
],
},
},
{
id: 'listIdOrSlug',
title: 'List ID or Slug',
type: 'short-input',
canonicalParamId: 'listIdOrSlug',
placeholder: 'Enter the list ID or slug',
mode: 'advanced',
condition: {
field: 'operation',
value: [
+84 -1
View File
@@ -65,11 +65,30 @@ export const CalComBlock: BlockConfig<ToolResponse> = {
},
// === Create Booking fields ===
{
id: 'eventTypeSelector',
title: 'Event Type',
type: 'project-selector',
canonicalParamId: 'eventTypeId',
serviceId: 'calcom',
selectorKey: 'calcom.eventTypes',
selectorAllowSearch: false,
placeholder: 'Select event type',
dependsOn: ['credential'],
mode: 'basic',
condition: {
field: 'operation',
value: ['calcom_create_booking', 'calcom_get_slots'],
},
required: { field: 'operation', value: 'calcom_create_booking' },
},
{
id: 'eventTypeId',
title: 'Event Type ID',
type: 'short-input',
canonicalParamId: 'eventTypeId',
placeholder: 'Enter event type ID (number)',
mode: 'advanced',
condition: {
field: 'operation',
value: ['calcom_create_booking', 'calcom_get_slots'],
@@ -261,11 +280,33 @@ Return ONLY the IANA timezone string - no explanations or quotes.`,
},
// === Event Type fields ===
{
id: 'eventTypeParamSelector',
title: 'Event Type',
type: 'project-selector',
canonicalParamId: 'eventTypeIdParam',
serviceId: 'calcom',
selectorKey: 'calcom.eventTypes',
selectorAllowSearch: false,
placeholder: 'Select event type',
dependsOn: ['credential'],
mode: 'basic',
condition: {
field: 'operation',
value: ['calcom_get_event_type', 'calcom_update_event_type', 'calcom_delete_event_type'],
},
required: {
field: 'operation',
value: ['calcom_get_event_type', 'calcom_update_event_type', 'calcom_delete_event_type'],
},
},
{
id: 'eventTypeIdParam',
title: 'Event Type ID',
type: 'short-input',
canonicalParamId: 'eventTypeIdParam',
placeholder: 'Enter event type ID',
mode: 'advanced',
condition: {
field: 'operation',
value: ['calcom_get_event_type', 'calcom_update_event_type', 'calcom_delete_event_type'],
@@ -364,10 +405,27 @@ Return ONLY the IANA timezone string - no explanations or quotes.`,
},
mode: 'advanced',
},
{
id: 'eventTypeScheduleSelector',
title: 'Schedule',
type: 'project-selector',
canonicalParamId: 'eventTypeScheduleId',
serviceId: 'calcom',
selectorKey: 'calcom.schedules',
selectorAllowSearch: false,
placeholder: 'Select schedule',
dependsOn: ['credential'],
mode: 'basic',
condition: {
field: 'operation',
value: ['calcom_create_event_type', 'calcom_update_event_type'],
},
},
{
id: 'eventTypeScheduleId',
title: 'Schedule ID',
type: 'short-input',
canonicalParamId: 'eventTypeScheduleId',
placeholder: 'Assign to a specific schedule',
condition: {
field: 'operation',
@@ -388,11 +446,33 @@ Return ONLY the IANA timezone string - no explanations or quotes.`,
},
// === Schedule fields ===
{
id: 'scheduleSelector',
title: 'Schedule',
type: 'project-selector',
canonicalParamId: 'scheduleId',
serviceId: 'calcom',
selectorKey: 'calcom.schedules',
selectorAllowSearch: false,
placeholder: 'Select schedule',
dependsOn: ['credential'],
mode: 'basic',
condition: {
field: 'operation',
value: ['calcom_get_schedule', 'calcom_update_schedule', 'calcom_delete_schedule'],
},
required: {
field: 'operation',
value: ['calcom_get_schedule', 'calcom_update_schedule', 'calcom_delete_schedule'],
},
},
{
id: 'scheduleId',
title: 'Schedule ID',
type: 'short-input',
canonicalParamId: 'scheduleId',
placeholder: 'Enter schedule ID',
mode: 'advanced',
condition: {
field: 'operation',
value: ['calcom_get_schedule', 'calcom_update_schedule', 'calcom_delete_schedule'],
@@ -771,7 +851,10 @@ Return ONLY valid JSON - no explanations.`,
cancellationReason: { type: 'string', description: 'Reason for cancellation' },
reschedulingReason: { type: 'string', description: 'Reason for rescheduling' },
bookingStatus: { type: 'string', description: 'Filter by booking status' },
eventTypeIdParam: { type: 'number', description: 'Event type ID for get/update/delete' },
eventTypeIdParam: {
type: 'number',
description: 'Event type ID for get/update/delete',
},
title: { type: 'string', description: 'Event type title' },
slug: { type: 'string', description: 'URL-friendly slug' },
eventLength: { type: 'number', description: 'Event duration in minutes' },
+34 -3
View File
@@ -84,7 +84,6 @@ export const ConfluenceBlock: BlockConfig<ConfluenceResponse> = {
'write:content.property:confluence',
'read:hierarchical-content:confluence',
'read:content.metadata:confluence',
'read:user:confluence',
],
placeholder: 'Select Confluence account',
required: true,
@@ -645,11 +644,44 @@ export const ConfluenceV2Block: BlockConfig<ConfluenceResponse> = {
],
},
},
{
id: 'spaceSelector',
title: 'Space',
type: 'project-selector',
canonicalParamId: 'spaceId',
serviceId: 'confluence',
selectorKey: 'confluence.spaces',
selectorAllowSearch: false,
placeholder: 'Select Confluence space',
dependsOn: ['credential', 'domain'],
mode: 'basic',
required: true,
condition: {
field: 'operation',
value: [
'create',
'get_space',
'update_space',
'delete_space',
'list_pages_in_space',
'search_in_space',
'create_blogpost',
'list_blogposts_in_space',
'list_space_labels',
'list_space_permissions',
'list_space_properties',
'create_space_property',
'delete_space_property',
],
},
},
{
id: 'spaceId',
title: 'Space ID',
type: 'short-input',
canonicalParamId: 'spaceId',
placeholder: 'Enter Confluence space ID',
mode: 'advanced',
required: true,
condition: {
field: 'operation',
@@ -1250,7 +1282,6 @@ export const ConfluenceV2Block: BlockConfig<ConfluenceResponse> = {
...rest
} = params
// Use canonical param (serializer already handles basic/advanced mode)
const effectivePageId = pageId ? String(pageId).trim() : ''
if (operation === 'add_label') {
@@ -1511,7 +1542,7 @@ export const ConfluenceV2Block: BlockConfig<ConfluenceResponse> = {
operation: { type: 'string', description: 'Operation to perform' },
domain: { type: 'string', description: 'Confluence domain' },
oauthCredential: { type: 'string', description: 'Confluence access token' },
pageId: { type: 'string', description: 'Page identifier (canonical param)' },
pageId: { type: 'string', description: 'Page identifier' },
spaceId: { type: 'string', description: 'Space identifier' },
blogPostId: { type: 'string', description: 'Blog post identifier' },
versionNumber: { type: 'number', description: 'Page version number' },
+32
View File
@@ -109,20 +109,52 @@ Return ONLY the SQL query - no explanations, no quotes, no extra text.`,
condition: { field: 'operation', value: 'query' },
},
{
id: 'datasetSelector',
title: 'Dataset',
type: 'project-selector',
canonicalParamId: 'datasetId',
serviceId: 'google-bigquery',
selectorKey: 'bigquery.datasets',
selectorAllowSearch: false,
placeholder: 'Select BigQuery dataset',
dependsOn: ['credential', 'projectId'],
mode: 'basic',
condition: { field: 'operation', value: ['list_tables', 'get_table', 'insert_rows'] },
required: { field: 'operation', value: ['list_tables', 'get_table', 'insert_rows'] },
},
{
id: 'datasetId',
title: 'Dataset ID',
type: 'short-input',
canonicalParamId: 'datasetId',
placeholder: 'Enter BigQuery dataset ID',
mode: 'advanced',
condition: { field: 'operation', value: ['list_tables', 'get_table', 'insert_rows'] },
required: { field: 'operation', value: ['list_tables', 'get_table', 'insert_rows'] },
},
{
id: 'tableSelector',
title: 'Table',
type: 'file-selector',
canonicalParamId: 'tableId',
serviceId: 'google-bigquery',
selectorKey: 'bigquery.tables',
selectorAllowSearch: false,
placeholder: 'Select BigQuery table',
dependsOn: ['credential', 'projectId', 'datasetSelector'],
mode: 'basic',
condition: { field: 'operation', value: ['get_table', 'insert_rows'] },
required: { field: 'operation', value: ['get_table', 'insert_rows'] },
},
{
id: 'tableId',
title: 'Table ID',
type: 'short-input',
canonicalParamId: 'tableId',
placeholder: 'Enter BigQuery table ID',
mode: 'advanced',
condition: { field: 'operation', value: ['get_table', 'insert_rows'] },
required: { field: 'operation', value: ['get_table', 'insert_rows'] },
},
+19 -2
View File
@@ -51,12 +51,27 @@ export const GoogleTasksBlock: BlockConfig<GoogleTasksResponse> = {
required: true,
},
// Task List ID - shown for all task operations (not list_task_lists)
// Task List - shown for all task operations (not list_task_lists)
{
id: 'taskListSelector',
title: 'Task List',
type: 'project-selector',
canonicalParamId: 'taskListId',
serviceId: 'google-tasks',
selectorKey: 'google.tasks.lists',
selectorAllowSearch: false,
placeholder: 'Select task list',
dependsOn: ['credential'],
mode: 'basic',
condition: { field: 'operation', value: 'list_task_lists', not: true },
},
{
id: 'taskListId',
title: 'Task List ID',
type: 'short-input',
canonicalParamId: 'taskListId',
placeholder: 'Task list ID (leave empty for default list)',
mode: 'advanced',
condition: { field: 'operation', value: 'list_task_lists', not: true },
},
@@ -210,7 +225,9 @@ Return ONLY the timestamp - no explanations, no extra text.`,
params: (params) => {
const { oauthCredential, operation, showCompleted, maxResults, ...rest } = params
const processedParams: Record<string, unknown> = { ...rest }
const processedParams: Record<string, unknown> = {
...rest,
}
if (maxResults && typeof maxResults === 'string') {
processedParams.maxResults = Number.parseInt(maxResults, 10)
@@ -107,10 +107,16 @@ export const JiraServiceManagementBlock: BlockConfig<JsmResponse> = {
required: true,
},
{
id: 'serviceDeskId',
title: 'Service Desk ID',
type: 'short-input',
placeholder: 'Enter service desk ID',
id: 'serviceDeskSelector',
title: 'Service Desk',
type: 'project-selector',
canonicalParamId: 'serviceDeskId',
serviceId: 'jira',
selectorKey: 'jsm.serviceDesks',
selectorAllowSearch: false,
placeholder: 'Select service desk',
dependsOn: ['credential', 'domain'],
mode: 'basic',
required: {
field: 'operation',
value: [
@@ -139,12 +145,63 @@ export const JiraServiceManagementBlock: BlockConfig<JsmResponse> = {
],
},
},
{
id: 'serviceDeskId',
title: 'Service Desk ID',
type: 'short-input',
canonicalParamId: 'serviceDeskId',
placeholder: 'Enter service desk ID',
mode: 'advanced',
required: {
field: 'operation',
value: [
'get_request_types',
'create_request',
'get_customers',
'add_customer',
'get_organizations',
'add_organization',
'get_queues',
'get_request_type_fields',
],
},
condition: {
field: 'operation',
value: [
'get_request_types',
'create_request',
'get_customers',
'add_customer',
'get_organizations',
'add_organization',
'get_queues',
'get_requests',
'get_request_type_fields',
],
},
},
{
id: 'requestTypeSelector',
title: 'Request Type',
type: 'file-selector',
canonicalParamId: 'requestTypeId',
serviceId: 'jira',
selectorKey: 'jsm.requestTypes',
selectorAllowSearch: false,
placeholder: 'Select request type',
dependsOn: ['credential', 'domain', 'serviceDeskSelector'],
mode: 'basic',
required: true,
condition: { field: 'operation', value: ['create_request', 'get_request_type_fields'] },
},
{
id: 'requestTypeId',
title: 'Request Type ID',
type: 'short-input',
canonicalParamId: 'requestTypeId',
required: true,
placeholder: 'Enter request type ID',
mode: 'advanced',
condition: { field: 'operation', value: ['create_request', 'get_request_type_fields'] },
},
{
+26 -2
View File
@@ -84,12 +84,36 @@ export const MicrosoftPlannerBlock: BlockConfig<MicrosoftPlannerResponse> = {
placeholder: 'Enter credential ID',
},
// Plan ID - for various operations
// Plan selector - basic mode
{
id: 'planSelector',
title: 'Plan',
type: 'project-selector',
canonicalParamId: 'planId',
serviceId: 'microsoft-planner',
selectorKey: 'microsoft.planner.plans',
selectorAllowSearch: false,
placeholder: 'Select a plan',
dependsOn: ['credential'],
mode: 'basic',
condition: {
field: 'operation',
value: ['create_task', 'read_task', 'read_plan', 'list_buckets', 'create_bucket'],
},
required: {
field: 'operation',
value: ['read_plan', 'list_buckets', 'create_bucket', 'create_task'],
},
},
// Plan ID - advanced mode
{
id: 'planId',
title: 'Plan ID',
type: 'short-input',
canonicalParamId: 'planId',
placeholder: 'Enter the plan ID',
mode: 'advanced',
condition: {
field: 'operation',
value: ['create_task', 'read_task', 'read_plan', 'list_buckets', 'create_bucket'],
@@ -110,7 +134,7 @@ export const MicrosoftPlannerBlock: BlockConfig<MicrosoftPlannerResponse> = {
serviceId: 'microsoft-planner',
selectorKey: 'microsoft.planner',
condition: { field: 'operation', value: ['read_task'] },
dependsOn: ['credential', 'planId'],
dependsOn: ['credential', 'planSelector'],
mode: 'basic',
canonicalParamId: 'readTaskId',
},
+53 -37
View File
@@ -53,15 +53,49 @@ export const NotionBlock: BlockConfig<NotionResponse> = {
placeholder: 'Enter credential ID',
required: true,
},
// Read/Write operation - Page ID
{
id: 'pageSelector',
title: 'Page',
type: 'file-selector',
canonicalParamId: 'pageId',
serviceId: 'notion',
selectorKey: 'notion.pages',
placeholder: 'Select Notion page',
dependsOn: ['credential'],
mode: 'basic',
condition: {
field: 'operation',
value: ['notion_read', 'notion_write'],
},
required: true,
},
{
id: 'pageId',
title: 'Page ID',
type: 'short-input',
canonicalParamId: 'pageId',
placeholder: 'Enter Notion page ID',
mode: 'advanced',
condition: {
field: 'operation',
value: 'notion_read',
value: ['notion_read', 'notion_write'],
},
required: true,
},
{
id: 'databaseSelector',
title: 'Database',
type: 'project-selector',
canonicalParamId: 'databaseId',
serviceId: 'notion',
selectorKey: 'notion.databases',
selectorAllowSearch: false,
placeholder: 'Select Notion database',
dependsOn: ['credential'],
mode: 'basic',
condition: {
field: 'operation',
value: ['notion_read_database', 'notion_query_database', 'notion_add_database_row'],
},
required: true,
},
@@ -69,31 +103,36 @@ export const NotionBlock: BlockConfig<NotionResponse> = {
id: 'databaseId',
title: 'Database ID',
type: 'short-input',
canonicalParamId: 'databaseId',
placeholder: 'Enter Notion database ID',
mode: 'advanced',
condition: {
field: 'operation',
value: 'notion_read_database',
value: ['notion_read_database', 'notion_query_database', 'notion_add_database_row'],
},
required: true,
},
{
id: 'pageId',
title: 'Page ID',
type: 'short-input',
placeholder: 'Enter Notion page ID',
condition: {
field: 'operation',
value: 'notion_write',
},
id: 'parentSelector',
title: 'Parent Page',
type: 'file-selector',
canonicalParamId: 'parentId',
serviceId: 'notion',
selectorKey: 'notion.pages',
placeholder: 'Select parent page',
dependsOn: ['credential'],
mode: 'basic',
condition: { field: 'operation', value: ['notion_create_page', 'notion_create_database'] },
required: true,
},
// Create operation fields
{
id: 'parentId',
title: 'Parent Page ID',
type: 'short-input',
canonicalParamId: 'parentId',
placeholder: 'ID of parent page',
condition: { field: 'operation', value: 'notion_create_page' },
mode: 'advanced',
condition: { field: 'operation', value: ['notion_create_page', 'notion_create_database'] },
required: true,
},
{
@@ -148,14 +187,6 @@ export const NotionBlock: BlockConfig<NotionResponse> = {
},
},
// Query Database Fields
{
id: 'databaseId',
title: 'Database ID',
type: 'short-input',
placeholder: 'Enter Notion database ID',
condition: { field: 'operation', value: 'notion_query_database' },
required: true,
},
{
id: 'filter',
title: 'Filter',
@@ -218,14 +249,6 @@ export const NotionBlock: BlockConfig<NotionResponse> = {
condition: { field: 'operation', value: 'notion_search' },
},
// Create Database Fields
{
id: 'parentId',
title: 'Parent Page ID',
type: 'short-input',
placeholder: 'ID of parent page where database will be created',
condition: { field: 'operation', value: 'notion_create_database' },
required: true,
},
{
id: 'title',
title: 'Database Title',
@@ -256,14 +279,6 @@ export const NotionBlock: BlockConfig<NotionResponse> = {
},
},
// Add Database Row Fields
{
id: 'databaseId',
title: 'Database ID',
type: 'short-input',
placeholder: 'Enter Notion database ID',
condition: { field: 'operation', value: 'notion_add_database_row' },
required: true,
},
{
id: 'properties',
title: 'Row Properties',
@@ -404,6 +419,7 @@ export const NotionBlock: BlockConfig<NotionResponse> = {
}
// V2 Block with API-aligned outputs
export const NotionV2Block: BlockConfig<any> = {
type: 'notion_v2',
name: 'Notion',
+25 -17
View File
@@ -96,12 +96,35 @@ export const PipedriveBlock: BlockConfig<PipedriveResponse> = {
placeholder: 'Filter by organization ID',
condition: { field: 'operation', value: ['get_all_deals'] },
},
{
id: 'pipelineSelector',
title: 'Pipeline',
type: 'project-selector',
canonicalParamId: 'pipeline_id',
serviceId: 'pipedrive',
selectorKey: 'pipedrive.pipelines',
selectorAllowSearch: false,
placeholder: 'Select pipeline',
dependsOn: ['credential'],
mode: 'basic',
condition: {
field: 'operation',
value: ['get_all_deals', 'create_deal', 'get_pipeline_deals'],
},
required: { field: 'operation', value: 'get_pipeline_deals' },
},
{
id: 'pipeline_id',
title: 'Pipeline ID',
type: 'short-input',
placeholder: 'Filter by pipeline ID ',
condition: { field: 'operation', value: ['get_all_deals'] },
canonicalParamId: 'pipeline_id',
placeholder: 'Enter pipeline ID',
mode: 'advanced',
condition: {
field: 'operation',
value: ['get_all_deals', 'create_deal', 'get_pipeline_deals'],
},
required: { field: 'operation', value: 'get_pipeline_deals' },
},
{
id: 'updated_since',
@@ -174,13 +197,6 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
placeholder: 'Associated organization ID ',
condition: { field: 'operation', value: ['create_deal'] },
},
{
id: 'pipeline_id',
title: 'Pipeline ID',
type: 'short-input',
placeholder: 'Pipeline ID ',
condition: { field: 'operation', value: ['create_deal'] },
},
{
id: 'stage_id',
title: 'Stage ID',
@@ -329,14 +345,6 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n
],
},
},
{
id: 'pipeline_id',
title: 'Pipeline ID',
type: 'short-input',
placeholder: 'Enter pipeline ID',
required: true,
condition: { field: 'operation', value: ['get_pipeline_deals'] },
},
{
id: 'stage_id',
title: 'Stage ID',
+25 -4
View File
@@ -112,12 +112,26 @@ export const SharepointBlock: BlockConfig<SharepointResponse> = {
mode: 'advanced',
},
{
id: 'listSelector',
title: 'List',
type: 'file-selector',
canonicalParamId: 'listId',
serviceId: 'sharepoint',
selectorKey: 'sharepoint.lists',
selectorAllowSearch: false,
placeholder: 'Select a list',
dependsOn: ['credential', 'siteSelector'],
mode: 'basic',
condition: { field: 'operation', value: ['read_list', 'update_list', 'add_list_items'] },
},
{
id: 'listId',
title: 'List ID',
type: 'short-input',
placeholder: 'Enter list ID (GUID). Required for Update; optional for Read.',
canonicalParamId: 'listId',
placeholder: 'Enter list ID (GUID). Required for Update; optional for Read.',
mode: 'advanced',
condition: { field: 'operation', value: ['read_list', 'update_list', 'add_list_items'] },
},
@@ -425,7 +439,9 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`,
includeColumns,
includeItems,
files, // canonical param from uploadFiles (basic) or files (advanced)
driveId, // canonical param from driveId
columnDefinitions,
listId,
...others
} = rest as any
@@ -457,7 +473,7 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`,
try {
logger.info('SharepointBlock list item param check', {
siteId: effectiveSiteId || undefined,
listId: (others as any)?.listId,
listId: listId,
listTitle: (others as any)?.listTitle,
itemId: sanitizedItemId,
hasItemFields: !!parsedItemFields && typeof parsedItemFields === 'object',
@@ -477,6 +493,8 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`,
pageSize: others.pageSize ? Number.parseInt(others.pageSize as string, 10) : undefined,
mimeType: mimeType,
...others,
...(listId ? { listId } : {}),
...(driveId ? { driveId } : {}),
itemId: sanitizedItemId,
listItemFields: parsedItemFields,
includeColumns: coerceBoolean(includeColumns),
@@ -517,10 +535,13 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`,
includeItems: { type: 'boolean', description: 'Include items in response' },
itemId: { type: 'string', description: 'List item ID (canonical param)' },
listItemFields: { type: 'string', description: 'List item fields (canonical param)' },
driveId: { type: 'string', description: 'Document library (drive) ID (canonical param)' },
driveId: {
type: 'string',
description: 'Document library (drive) ID',
},
folderPath: { type: 'string', description: 'Folder path for file upload' },
fileName: { type: 'string', description: 'File name override' },
files: { type: 'array', description: 'Files to upload (canonical param)' },
files: { type: 'array', description: 'Files to upload' },
},
outputs: {
sites: {
+33 -30
View File
@@ -59,26 +59,50 @@ export const TrelloBlock: BlockConfig<ToolResponse> = {
},
{
id: 'boardId',
id: 'boardSelector',
title: 'Board',
type: 'short-input',
placeholder: 'Enter board ID',
type: 'project-selector',
canonicalParamId: 'boardId',
serviceId: 'trello',
selectorKey: 'trello.boards',
selectorAllowSearch: false,
placeholder: 'Select Trello board',
dependsOn: ['credential'],
mode: 'basic',
condition: {
field: 'operation',
value: 'trello_list_lists',
value: [
'trello_list_lists',
'trello_list_cards',
'trello_create_card',
'trello_get_actions',
],
},
required: {
field: 'operation',
value: ['trello_list_lists', 'trello_list_cards', 'trello_create_card'],
},
required: true,
},
{
id: 'boardId',
title: 'Board',
title: 'Board ID',
type: 'short-input',
placeholder: 'Enter board ID or search for a board',
canonicalParamId: 'boardId',
placeholder: 'Enter board ID',
mode: 'advanced',
condition: {
field: 'operation',
value: 'trello_list_cards',
value: [
'trello_list_lists',
'trello_list_cards',
'trello_create_card',
'trello_get_actions',
],
},
required: {
field: 'operation',
value: ['trello_list_lists', 'trello_list_cards', 'trello_create_card'],
},
required: true,
},
{
id: 'listId',
@@ -90,17 +114,6 @@ export const TrelloBlock: BlockConfig<ToolResponse> = {
value: 'trello_list_cards',
},
},
{
id: 'boardId',
title: 'Board',
type: 'short-input',
placeholder: 'Enter board ID or search for a board',
condition: {
field: 'operation',
value: 'trello_create_card',
},
required: true,
},
{
id: 'listId',
title: 'List',
@@ -278,16 +291,6 @@ Return ONLY the date/timestamp string - no explanations, no quotes, no extra tex
},
},
{
id: 'boardId',
title: 'Board ID',
type: 'short-input',
placeholder: 'Enter board ID to get board actions',
condition: {
field: 'operation',
value: 'trello_get_actions',
},
},
{
id: 'cardId',
title: 'Card ID',
+28 -2
View File
@@ -77,12 +77,39 @@ export const ZoomBlock: BlockConfig<ZoomResponse> = {
value: ['zoom_create_meeting', 'zoom_list_meetings', 'zoom_list_recordings'],
},
},
// Meeting ID for get/update/delete/invitation/recordings/participants operations
// Meeting selector for get/update/delete/invitation/recordings/participants operations
{
id: 'meetingSelector',
title: 'Meeting',
type: 'project-selector',
canonicalParamId: 'meetingId',
serviceId: 'zoom',
selectorKey: 'zoom.meetings',
selectorAllowSearch: true,
placeholder: 'Select Zoom meeting',
dependsOn: ['credential'],
mode: 'basic',
required: true,
condition: {
field: 'operation',
value: [
'zoom_get_meeting',
'zoom_update_meeting',
'zoom_delete_meeting',
'zoom_get_meeting_invitation',
'zoom_get_meeting_recordings',
'zoom_delete_recording',
'zoom_list_past_participants',
],
},
},
{
id: 'meetingId',
title: 'Meeting ID',
type: 'short-input',
canonicalParamId: 'meetingId',
placeholder: 'Enter meeting ID',
mode: 'advanced',
required: true,
condition: {
field: 'operation',
@@ -114,7 +141,6 @@ export const ZoomBlock: BlockConfig<ZoomResponse> = {
title: 'Topic',
type: 'short-input',
placeholder: 'Meeting topic (optional)',
mode: 'advanced',
condition: {
field: 'operation',
value: ['zoom_update_meeting'],
+834 -5
View File
@@ -10,6 +10,29 @@ import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
const SELECTOR_STALE = 60 * 1000
type AirtableBase = { id: string; name: string }
type AirtableTable = { id: string; name: string }
type AsanaWorkspace = { id: string; name: string }
type AttioObject = { id: string; name: string }
type AttioList = { id: string; name: string }
type BigQueryDataset = {
datasetReference: { datasetId: string; projectId: string }
friendlyName?: string
}
type BigQueryTable = { tableReference: { tableId: string }; friendlyName?: string }
type CalcomEventType = { id: string; title: string; slug: string }
type ConfluenceSpace = { id: string; name: string; key: string }
type JsmServiceDesk = { id: string; name: string }
type JsmRequestType = { id: string; name: string }
type NotionDatabase = { id: string; name: string }
type NotionPage = { id: string; name: string }
type PipedrivePipeline = { id: string; name: string }
type ZoomMeeting = { id: string; name: string }
type CalcomSchedule = { id: string; name: string }
type GoogleTaskList = { id: string; title: string }
type PlannerPlan = { id: string; title: string }
type SharepointList = { id: string; displayName: string }
type TrelloBoard = { id: string; name: string; closed?: boolean }
type SlackChannel = { id: string; name: string }
type SlackUser = { id: string; name: string; real_name: string }
type FolderResponse = { id: string; name: string }
@@ -37,6 +60,768 @@ const ensureKnowledgeBase = (context: SelectorContext): string => {
}
const registry: Record<SelectorKey, SelectorDefinition> = {
'airtable.bases': {
key: 'airtable.bases',
staleTime: SELECTOR_STALE,
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'airtable.bases',
context.credentialId ?? 'none',
],
enabled: ({ context }) => Boolean(context.credentialId),
fetchList: async ({ context }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'airtable.bases')
const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId })
const data = await fetchJson<{ bases: AirtableBase[] }>('/api/tools/airtable/bases', {
method: 'POST',
body,
})
return (data.bases || []).map((base) => ({
id: base.id,
label: base.name,
}))
},
fetchById: async ({ context, detailId }: SelectorQueryArgs) => {
if (!detailId) return null
const credentialId = ensureCredential(context, 'airtable.bases')
const body = JSON.stringify({
credential: credentialId,
workflowId: context.workflowId,
baseId: detailId,
})
const data = await fetchJson<{ bases: AirtableBase[] }>('/api/tools/airtable/bases', {
method: 'POST',
body,
})
const base = (data.bases || []).find((b) => b.id === detailId) ?? null
if (!base) return null
return { id: base.id, label: base.name }
},
},
'airtable.tables': {
key: 'airtable.tables',
staleTime: SELECTOR_STALE,
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'airtable.tables',
context.credentialId ?? 'none',
context.baseId ?? 'none',
],
enabled: ({ context }) => Boolean(context.credentialId && context.baseId),
fetchList: async ({ context }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'airtable.tables')
if (!context.baseId) {
throw new Error('Missing base ID for airtable.tables selector')
}
const body = JSON.stringify({
credential: credentialId,
workflowId: context.workflowId,
baseId: context.baseId,
})
const data = await fetchJson<{ tables: AirtableTable[] }>('/api/tools/airtable/tables', {
method: 'POST',
body,
})
return (data.tables || []).map((table) => ({
id: table.id,
label: table.name,
}))
},
fetchById: async ({ context, detailId }: SelectorQueryArgs) => {
if (!detailId) return null
const credentialId = ensureCredential(context, 'airtable.tables')
if (!context.baseId) return null
const body = JSON.stringify({
credential: credentialId,
workflowId: context.workflowId,
baseId: context.baseId,
})
const data = await fetchJson<{ tables: AirtableTable[] }>('/api/tools/airtable/tables', {
method: 'POST',
body,
})
const table = (data.tables || []).find((t) => t.id === detailId) ?? null
if (!table) return null
return { id: table.id, label: table.name }
},
},
'asana.workspaces': {
key: 'asana.workspaces',
staleTime: SELECTOR_STALE,
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'asana.workspaces',
context.credentialId ?? 'none',
],
enabled: ({ context }) => Boolean(context.credentialId),
fetchList: async ({ context }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'asana.workspaces')
const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId })
const data = await fetchJson<{ workspaces: AsanaWorkspace[] }>(
'/api/tools/asana/workspaces',
{ method: 'POST', body }
)
return (data.workspaces || []).map((ws) => ({ id: ws.id, label: ws.name }))
},
fetchById: async ({ context, detailId }: SelectorQueryArgs) => {
if (!detailId) return null
const credentialId = ensureCredential(context, 'asana.workspaces')
const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId })
const data = await fetchJson<{ workspaces: AsanaWorkspace[] }>(
'/api/tools/asana/workspaces',
{ method: 'POST', body }
)
const ws = (data.workspaces || []).find((w) => w.id === detailId) ?? null
if (!ws) return null
return { id: ws.id, label: ws.name }
},
},
'attio.objects': {
key: 'attio.objects',
staleTime: SELECTOR_STALE,
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'attio.objects',
context.credentialId ?? 'none',
],
enabled: ({ context }) => Boolean(context.credentialId),
fetchList: async ({ context }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'attio.objects')
const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId })
const data = await fetchJson<{ objects: AttioObject[] }>('/api/tools/attio/objects', {
method: 'POST',
body,
})
return (data.objects || []).map((obj) => ({
id: obj.id,
label: obj.name,
}))
},
fetchById: async ({ context, detailId }: SelectorQueryArgs) => {
if (!detailId) return null
const credentialId = ensureCredential(context, 'attio.objects')
const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId })
const data = await fetchJson<{ objects: AttioObject[] }>('/api/tools/attio/objects', {
method: 'POST',
body,
})
const obj = (data.objects || []).find((o) => o.id === detailId) ?? null
if (!obj) return null
return { id: obj.id, label: obj.name }
},
},
'attio.lists': {
key: 'attio.lists',
staleTime: SELECTOR_STALE,
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'attio.lists',
context.credentialId ?? 'none',
],
enabled: ({ context }) => Boolean(context.credentialId),
fetchList: async ({ context }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'attio.lists')
const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId })
const data = await fetchJson<{ lists: AttioList[] }>('/api/tools/attio/lists', {
method: 'POST',
body,
})
return (data.lists || []).map((list) => ({
id: list.id,
label: list.name,
}))
},
fetchById: async ({ context, detailId }: SelectorQueryArgs) => {
if (!detailId) return null
const credentialId = ensureCredential(context, 'attio.lists')
const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId })
const data = await fetchJson<{ lists: AttioList[] }>('/api/tools/attio/lists', {
method: 'POST',
body,
})
const list = (data.lists || []).find((l) => l.id === detailId) ?? null
if (!list) return null
return { id: list.id, label: list.name }
},
},
'bigquery.datasets': {
key: 'bigquery.datasets',
staleTime: SELECTOR_STALE,
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'bigquery.datasets',
context.credentialId ?? 'none',
context.projectId ?? 'none',
],
enabled: ({ context }) => Boolean(context.credentialId && context.projectId),
fetchList: async ({ context }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'bigquery.datasets')
if (!context.projectId) throw new Error('Missing project ID for bigquery.datasets selector')
const body = JSON.stringify({
credential: credentialId,
workflowId: context.workflowId,
projectId: context.projectId,
})
const data = await fetchJson<{ datasets: BigQueryDataset[] }>(
'/api/tools/google_bigquery/datasets',
{ method: 'POST', body }
)
return (data.datasets || []).map((ds) => ({
id: ds.datasetReference.datasetId,
label: ds.friendlyName || ds.datasetReference.datasetId,
}))
},
fetchById: async ({ context, detailId }: SelectorQueryArgs) => {
if (!detailId || !context.projectId) return null
const credentialId = ensureCredential(context, 'bigquery.datasets')
const body = JSON.stringify({
credential: credentialId,
workflowId: context.workflowId,
projectId: context.projectId,
})
const data = await fetchJson<{ datasets: BigQueryDataset[] }>(
'/api/tools/google_bigquery/datasets',
{ method: 'POST', body }
)
const ds =
(data.datasets || []).find((d) => d.datasetReference.datasetId === detailId) ?? null
if (!ds) return null
return {
id: ds.datasetReference.datasetId,
label: ds.friendlyName || ds.datasetReference.datasetId,
}
},
},
'bigquery.tables': {
key: 'bigquery.tables',
staleTime: SELECTOR_STALE,
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'bigquery.tables',
context.credentialId ?? 'none',
context.projectId ?? 'none',
context.datasetId ?? 'none',
],
enabled: ({ context }) =>
Boolean(context.credentialId && context.projectId && context.datasetId),
fetchList: async ({ context }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'bigquery.tables')
if (!context.projectId) throw new Error('Missing project ID for bigquery.tables selector')
if (!context.datasetId) throw new Error('Missing dataset ID for bigquery.tables selector')
const body = JSON.stringify({
credential: credentialId,
workflowId: context.workflowId,
projectId: context.projectId,
datasetId: context.datasetId,
})
const data = await fetchJson<{ tables: BigQueryTable[] }>(
'/api/tools/google_bigquery/tables',
{ method: 'POST', body }
)
return (data.tables || []).map((t) => ({
id: t.tableReference.tableId,
label: t.friendlyName || t.tableReference.tableId,
}))
},
fetchById: async ({ context, detailId }: SelectorQueryArgs) => {
if (!detailId || !context.projectId || !context.datasetId) return null
const credentialId = ensureCredential(context, 'bigquery.tables')
const body = JSON.stringify({
credential: credentialId,
workflowId: context.workflowId,
projectId: context.projectId,
datasetId: context.datasetId,
})
const data = await fetchJson<{ tables: BigQueryTable[] }>(
'/api/tools/google_bigquery/tables',
{ method: 'POST', body }
)
const t = (data.tables || []).find((tbl) => tbl.tableReference.tableId === detailId) ?? null
if (!t) return null
return { id: t.tableReference.tableId, label: t.friendlyName || t.tableReference.tableId }
},
},
'calcom.eventTypes': {
key: 'calcom.eventTypes',
staleTime: SELECTOR_STALE,
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'calcom.eventTypes',
context.credentialId ?? 'none',
],
enabled: ({ context }) => Boolean(context.credentialId),
fetchList: async ({ context }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'calcom.eventTypes')
const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId })
const data = await fetchJson<{ eventTypes: CalcomEventType[] }>(
'/api/tools/calcom/event-types',
{ method: 'POST', body }
)
return (data.eventTypes || []).map((et) => ({
id: et.id,
label: et.title || et.slug,
}))
},
fetchById: async ({ context, detailId }: SelectorQueryArgs) => {
if (!detailId) return null
const credentialId = ensureCredential(context, 'calcom.eventTypes')
const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId })
const data = await fetchJson<{ eventTypes: CalcomEventType[] }>(
'/api/tools/calcom/event-types',
{ method: 'POST', body }
)
const et = (data.eventTypes || []).find((e) => e.id === detailId) ?? null
if (!et) return null
return { id: et.id, label: et.title || et.slug }
},
},
'calcom.schedules': {
key: 'calcom.schedules',
staleTime: SELECTOR_STALE,
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'calcom.schedules',
context.credentialId ?? 'none',
],
enabled: ({ context }) => Boolean(context.credentialId),
fetchList: async ({ context }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'calcom.schedules')
const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId })
const data = await fetchJson<{ schedules: CalcomSchedule[] }>('/api/tools/calcom/schedules', {
method: 'POST',
body,
})
return (data.schedules || []).map((s) => ({
id: s.id,
label: s.name,
}))
},
fetchById: async ({ context, detailId }: SelectorQueryArgs) => {
if (!detailId) return null
const credentialId = ensureCredential(context, 'calcom.schedules')
const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId })
const data = await fetchJson<{ schedules: CalcomSchedule[] }>('/api/tools/calcom/schedules', {
method: 'POST',
body,
})
const s = (data.schedules || []).find((sc) => sc.id === detailId) ?? null
if (!s) return null
return { id: s.id, label: s.name }
},
},
'confluence.spaces': {
key: 'confluence.spaces',
staleTime: SELECTOR_STALE,
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'confluence.spaces',
context.credentialId ?? 'none',
context.domain ?? 'none',
],
enabled: ({ context }) => Boolean(context.credentialId && context.domain),
fetchList: async ({ context }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'confluence.spaces')
const domain = ensureDomain(context, 'confluence.spaces')
const body = JSON.stringify({
credential: credentialId,
workflowId: context.workflowId,
domain,
})
const data = await fetchJson<{ spaces: ConfluenceSpace[] }>(
'/api/tools/confluence/selector-spaces',
{ method: 'POST', body }
)
return (data.spaces || []).map((space) => ({
id: space.id,
label: `${space.name} (${space.key})`,
}))
},
fetchById: async ({ context, detailId }: SelectorQueryArgs) => {
if (!detailId) return null
const credentialId = ensureCredential(context, 'confluence.spaces')
const domain = ensureDomain(context, 'confluence.spaces')
const body = JSON.stringify({
credential: credentialId,
workflowId: context.workflowId,
domain,
})
const data = await fetchJson<{ spaces: ConfluenceSpace[] }>(
'/api/tools/confluence/selector-spaces',
{ method: 'POST', body }
)
const space = (data.spaces || []).find((s) => s.id === detailId) ?? null
if (!space) return null
return { id: space.id, label: `${space.name} (${space.key})` }
},
},
'jsm.serviceDesks': {
key: 'jsm.serviceDesks',
staleTime: SELECTOR_STALE,
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'jsm.serviceDesks',
context.credentialId ?? 'none',
context.domain ?? 'none',
],
enabled: ({ context }) => Boolean(context.credentialId && context.domain),
fetchList: async ({ context }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'jsm.serviceDesks')
const domain = ensureDomain(context, 'jsm.serviceDesks')
const body = JSON.stringify({
credential: credentialId,
workflowId: context.workflowId,
domain,
})
const data = await fetchJson<{ serviceDesks: JsmServiceDesk[] }>(
'/api/tools/jsm/selector-servicedesks',
{ method: 'POST', body }
)
return (data.serviceDesks || []).map((sd) => ({
id: sd.id,
label: sd.name,
}))
},
fetchById: async ({ context, detailId }: SelectorQueryArgs) => {
if (!detailId) return null
const credentialId = ensureCredential(context, 'jsm.serviceDesks')
const domain = ensureDomain(context, 'jsm.serviceDesks')
const body = JSON.stringify({
credential: credentialId,
workflowId: context.workflowId,
domain,
})
const data = await fetchJson<{ serviceDesks: JsmServiceDesk[] }>(
'/api/tools/jsm/selector-servicedesks',
{ method: 'POST', body }
)
const sd = (data.serviceDesks || []).find((s) => s.id === detailId) ?? null
if (!sd) return null
return { id: sd.id, label: sd.name }
},
},
'jsm.requestTypes': {
key: 'jsm.requestTypes',
staleTime: SELECTOR_STALE,
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'jsm.requestTypes',
context.credentialId ?? 'none',
context.domain ?? 'none',
context.serviceDeskId ?? 'none',
],
enabled: ({ context }) =>
Boolean(context.credentialId && context.domain && context.serviceDeskId),
fetchList: async ({ context }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'jsm.requestTypes')
const domain = ensureDomain(context, 'jsm.requestTypes')
if (!context.serviceDeskId) throw new Error('Missing serviceDeskId for jsm.requestTypes')
const body = JSON.stringify({
credential: credentialId,
workflowId: context.workflowId,
domain,
serviceDeskId: context.serviceDeskId,
})
const data = await fetchJson<{ requestTypes: JsmRequestType[] }>(
'/api/tools/jsm/selector-requesttypes',
{ method: 'POST', body }
)
return (data.requestTypes || []).map((rt) => ({
id: rt.id,
label: rt.name,
}))
},
fetchById: async ({ context, detailId }: SelectorQueryArgs) => {
if (!detailId) return null
const credentialId = ensureCredential(context, 'jsm.requestTypes')
const domain = ensureDomain(context, 'jsm.requestTypes')
if (!context.serviceDeskId) return null
const body = JSON.stringify({
credential: credentialId,
workflowId: context.workflowId,
domain,
serviceDeskId: context.serviceDeskId,
})
const data = await fetchJson<{ requestTypes: JsmRequestType[] }>(
'/api/tools/jsm/selector-requesttypes',
{ method: 'POST', body }
)
const rt = (data.requestTypes || []).find((r) => r.id === detailId) ?? null
if (!rt) return null
return { id: rt.id, label: rt.name }
},
},
'google.tasks.lists': {
key: 'google.tasks.lists',
staleTime: SELECTOR_STALE,
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'google.tasks.lists',
context.credentialId ?? 'none',
],
enabled: ({ context }) => Boolean(context.credentialId),
fetchList: async ({ context }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'google.tasks.lists')
const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId })
const data = await fetchJson<{ taskLists: GoogleTaskList[] }>(
'/api/tools/google_tasks/task-lists',
{ method: 'POST', body }
)
return (data.taskLists || []).map((tl) => ({ id: tl.id, label: tl.title }))
},
fetchById: async ({ context, detailId }: SelectorQueryArgs) => {
if (!detailId) return null
const credentialId = ensureCredential(context, 'google.tasks.lists')
const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId })
const data = await fetchJson<{ taskLists: GoogleTaskList[] }>(
'/api/tools/google_tasks/task-lists',
{ method: 'POST', body }
)
const tl = (data.taskLists || []).find((t) => t.id === detailId) ?? null
if (!tl) return null
return { id: tl.id, label: tl.title }
},
},
'microsoft.planner.plans': {
key: 'microsoft.planner.plans',
staleTime: SELECTOR_STALE,
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'microsoft.planner.plans',
context.credentialId ?? 'none',
],
enabled: ({ context }) => Boolean(context.credentialId),
fetchList: async ({ context }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'microsoft.planner.plans')
const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId })
const data = await fetchJson<{ plans: PlannerPlan[] }>('/api/tools/microsoft_planner/plans', {
method: 'POST',
body,
})
return (data.plans || []).map((plan) => ({ id: plan.id, label: plan.title }))
},
fetchById: async ({ context, detailId }: SelectorQueryArgs) => {
if (!detailId) return null
const credentialId = ensureCredential(context, 'microsoft.planner.plans')
const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId })
const data = await fetchJson<{ plans: PlannerPlan[] }>('/api/tools/microsoft_planner/plans', {
method: 'POST',
body,
})
const plan = (data.plans || []).find((p) => p.id === detailId) ?? null
if (!plan) return null
return { id: plan.id, label: plan.title }
},
},
'notion.databases': {
key: 'notion.databases',
staleTime: SELECTOR_STALE,
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'notion.databases',
context.credentialId ?? 'none',
],
enabled: ({ context }) => Boolean(context.credentialId),
fetchList: async ({ context }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'notion.databases')
const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId })
const data = await fetchJson<{ databases: NotionDatabase[] }>('/api/tools/notion/databases', {
method: 'POST',
body,
})
return (data.databases || []).map((db) => ({
id: db.id,
label: db.name,
}))
},
fetchById: async ({ context, detailId }: SelectorQueryArgs) => {
if (!detailId) return null
const credentialId = ensureCredential(context, 'notion.databases')
const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId })
const data = await fetchJson<{ databases: NotionDatabase[] }>('/api/tools/notion/databases', {
method: 'POST',
body,
})
const db = (data.databases || []).find((d) => d.id === detailId) ?? null
if (!db) return null
return { id: db.id, label: db.name }
},
},
'notion.pages': {
key: 'notion.pages',
staleTime: SELECTOR_STALE,
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'notion.pages',
context.credentialId ?? 'none',
],
enabled: ({ context }) => Boolean(context.credentialId),
fetchList: async ({ context }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'notion.pages')
const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId })
const data = await fetchJson<{ pages: NotionPage[] }>('/api/tools/notion/pages', {
method: 'POST',
body,
})
return (data.pages || []).map((page) => ({
id: page.id,
label: page.name,
}))
},
fetchById: async ({ context, detailId }: SelectorQueryArgs) => {
if (!detailId) return null
const credentialId = ensureCredential(context, 'notion.pages')
const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId })
const data = await fetchJson<{ pages: NotionPage[] }>('/api/tools/notion/pages', {
method: 'POST',
body,
})
const page = (data.pages || []).find((p) => p.id === detailId) ?? null
if (!page) return null
return { id: page.id, label: page.name }
},
},
'pipedrive.pipelines': {
key: 'pipedrive.pipelines',
staleTime: SELECTOR_STALE,
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'pipedrive.pipelines',
context.credentialId ?? 'none',
],
enabled: ({ context }) => Boolean(context.credentialId),
fetchList: async ({ context }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'pipedrive.pipelines')
const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId })
const data = await fetchJson<{ pipelines: PipedrivePipeline[] }>(
'/api/tools/pipedrive/pipelines',
{ method: 'POST', body }
)
return (data.pipelines || []).map((p) => ({
id: p.id,
label: p.name,
}))
},
fetchById: async ({ context, detailId }: SelectorQueryArgs) => {
if (!detailId) return null
const credentialId = ensureCredential(context, 'pipedrive.pipelines')
const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId })
const data = await fetchJson<{ pipelines: PipedrivePipeline[] }>(
'/api/tools/pipedrive/pipelines',
{ method: 'POST', body }
)
const p = (data.pipelines || []).find((pl) => pl.id === detailId) ?? null
if (!p) return null
return { id: p.id, label: p.name }
},
},
'sharepoint.lists': {
key: 'sharepoint.lists',
staleTime: SELECTOR_STALE,
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'sharepoint.lists',
context.credentialId ?? 'none',
context.siteId ?? 'none',
],
enabled: ({ context }) => Boolean(context.credentialId && context.siteId),
fetchList: async ({ context }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'sharepoint.lists')
if (!context.siteId) throw new Error('Missing site ID for sharepoint.lists selector')
const body = JSON.stringify({
credential: credentialId,
workflowId: context.workflowId,
siteId: context.siteId,
})
const data = await fetchJson<{ lists: SharepointList[] }>('/api/tools/sharepoint/lists', {
method: 'POST',
body,
})
return (data.lists || []).map((list) => ({ id: list.id, label: list.displayName }))
},
fetchById: async ({ context, detailId }: SelectorQueryArgs) => {
if (!detailId || !context.siteId) return null
const credentialId = ensureCredential(context, 'sharepoint.lists')
const body = JSON.stringify({
credential: credentialId,
workflowId: context.workflowId,
siteId: context.siteId,
})
const data = await fetchJson<{ lists: SharepointList[] }>('/api/tools/sharepoint/lists', {
method: 'POST',
body,
})
const list = (data.lists || []).find((l) => l.id === detailId) ?? null
if (!list) return null
return { id: list.id, label: list.displayName }
},
},
'trello.boards': {
key: 'trello.boards',
staleTime: SELECTOR_STALE,
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'trello.boards',
context.credentialId ?? 'none',
],
enabled: ({ context }) => Boolean(context.credentialId),
fetchList: async ({ context }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'trello.boards')
const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId })
const data = await fetchJson<{ boards: TrelloBoard[] }>('/api/tools/trello/boards', {
method: 'POST',
body,
})
return (data.boards || [])
.filter((board) => !board.closed)
.map((board) => ({ id: board.id, label: board.name }))
},
fetchById: async ({ context, detailId }: SelectorQueryArgs) => {
if (!detailId) return null
const credentialId = ensureCredential(context, 'trello.boards')
const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId })
const data = await fetchJson<{ boards: TrelloBoard[] }>('/api/tools/trello/boards', {
method: 'POST',
body,
})
const board = (data.boards || []).find((b) => b.id === detailId) ?? null
if (!board) return null
return { id: board.id, label: board.name }
},
},
'zoom.meetings': {
key: 'zoom.meetings',
staleTime: SELECTOR_STALE,
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'zoom.meetings',
context.credentialId ?? 'none',
],
enabled: ({ context }) => Boolean(context.credentialId),
fetchList: async ({ context }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'zoom.meetings')
const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId })
const data = await fetchJson<{ meetings: ZoomMeeting[] }>('/api/tools/zoom/meetings', {
method: 'POST',
body,
})
return (data.meetings || []).map((m) => ({
id: m.id,
label: m.name || `Meeting ${m.id}`,
}))
},
fetchById: async ({ context, detailId }: SelectorQueryArgs) => {
if (!detailId) return null
const credentialId = ensureCredential(context, 'zoom.meetings')
const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId })
const data = await fetchJson<{ meetings: ZoomMeeting[] }>('/api/tools/zoom/meetings', {
method: 'POST',
body,
})
const meeting = (data.meetings || []).find((m) => m.id === detailId) ?? null
if (!meeting) return null
return { id: meeting.id, label: meeting.name || `Meeting ${meeting.id}` }
},
},
'slack.channels': {
key: 'slack.channels',
staleTime: SELECTOR_STALE,
@@ -242,10 +1027,16 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
],
enabled: ({ context }) => Boolean(context.credentialId),
fetchList: async ({ context }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'sharepoint.sites')
const body = JSON.stringify({
credential: credentialId,
workflowId: context.workflowId,
})
const data = await fetchJson<{ files: { id: string; name: string }[] }>(
'/api/tools/sharepoint/sites',
{
searchParams: { credentialId: context.credentialId },
method: 'POST',
body,
}
)
return (data.files || []).map((file) => ({
@@ -253,6 +1044,24 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
label: file.name,
}))
},
fetchById: async ({ context, detailId }: SelectorQueryArgs) => {
if (!detailId) return null
const credentialId = ensureCredential(context, 'sharepoint.sites')
const body = JSON.stringify({
credential: credentialId,
workflowId: context.workflowId,
})
const data = await fetchJson<{ files: { id: string; name: string }[] }>(
'/api/tools/sharepoint/sites',
{
method: 'POST',
body,
}
)
const site = (data.files || []).find((f) => f.id === detailId) ?? null
if (!site) return null
return { id: site.id, label: site.name }
},
},
'microsoft.planner': {
key: 'microsoft.planner',
@@ -265,17 +1074,37 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
],
enabled: ({ context }) => Boolean(context.credentialId && context.planId),
fetchList: async ({ context }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'microsoft.planner')
const body = JSON.stringify({
credential: credentialId,
workflowId: context.workflowId,
planId: context.planId,
})
const data = await fetchJson<{ tasks: PlannerTask[] }>('/api/tools/microsoft_planner/tasks', {
searchParams: {
credentialId: context.credentialId,
planId: context.planId,
},
method: 'POST',
body,
})
return (data.tasks || []).map((task) => ({
id: task.id,
label: task.title,
}))
},
fetchById: async ({ context, detailId }: SelectorQueryArgs) => {
if (!detailId) return null
const credentialId = ensureCredential(context, 'microsoft.planner')
const body = JSON.stringify({
credential: credentialId,
workflowId: context.workflowId,
planId: context.planId,
})
const data = await fetchJson<{ tasks: PlannerTask[] }>('/api/tools/microsoft_planner/tasks', {
method: 'POST',
body,
})
const task = (data.tasks || []).find((t) => t.id === detailId) ?? null
if (!task) return null
return { id: task.id, label: task.title }
},
},
'jira.projects': {
key: 'jira.projects',
+8
View File
@@ -18,6 +18,10 @@ export interface SelectorResolutionArgs {
siteId?: string
collectionId?: string
spreadsheetId?: string
fileId?: string
baseId?: string
datasetId?: string
serviceDeskId?: string
}
export function resolveSelectorForSubBlock(
@@ -38,6 +42,10 @@ export function resolveSelectorForSubBlock(
siteId: args.siteId,
collectionId: args.collectionId,
spreadsheetId: args.spreadsheetId,
fileId: args.fileId,
baseId: args.baseId,
datasetId: args.datasetId,
serviceDeskId: args.serviceDeskId,
mimeType: subBlock.mimeType,
},
allowSearch: subBlock.selectorAllowSearch ?? true,
+23
View File
@@ -2,6 +2,26 @@ import type React from 'react'
import type { QueryKey } from '@tanstack/react-query'
export type SelectorKey =
| 'airtable.bases'
| 'airtable.tables'
| 'asana.workspaces'
| 'attio.lists'
| 'attio.objects'
| 'bigquery.datasets'
| 'bigquery.tables'
| 'calcom.eventTypes'
| 'calcom.schedules'
| 'confluence.spaces'
| 'google.tasks.lists'
| 'jsm.requestTypes'
| 'jsm.serviceDesks'
| 'microsoft.planner.plans'
| 'notion.databases'
| 'notion.pages'
| 'pipedrive.pipelines'
| 'sharepoint.lists'
| 'trello.boards'
| 'zoom.meetings'
| 'slack.channels'
| 'slack.users'
| 'gmail.labels'
@@ -54,6 +74,9 @@ export interface SelectorContext {
collectionId?: string
spreadsheetId?: string
excludeWorkflowId?: string
baseId?: string
datasetId?: string
serviceDeskId?: string
}
export interface SelectorQueryArgs {
@@ -1,5 +1,6 @@
import { useMemo } from 'react'
import { useQuery } from '@tanstack/react-query'
import { isEnvVarReference, isReference } from '@/executor/constants'
import { getSelectorDefinition, mergeOption } from '@/hooks/selectors/registry'
import type { SelectorKey, SelectorOption, SelectorQueryArgs } from '@/hooks/selectors/types'
@@ -35,8 +36,10 @@ export function useSelectorOptionDetail(
context: args.context,
detailId: args.detailId,
}
const hasRealDetailId =
Boolean(args.detailId) && !isReference(args.detailId!) && !isEnvVarReference(args.detailId!)
const baseEnabled =
Boolean(args.detailId) && definition.fetchById !== undefined
hasRealDetailId && definition.fetchById !== undefined
? definition.enabled
? definition.enabled(queryArgs)
: true
@@ -18,6 +18,13 @@ interface SelectorDisplayNameArgs {
planId?: string
teamId?: string
knowledgeBaseId?: string
baseId?: string
datasetId?: string
serviceDeskId?: string
siteId?: string
collectionId?: string
spreadsheetId?: string
fileId?: string
}
export function useSelectorDisplayName({
@@ -30,6 +37,13 @@ export function useSelectorDisplayName({
planId,
teamId,
knowledgeBaseId,
baseId,
datasetId,
serviceDeskId,
siteId,
collectionId,
spreadsheetId,
fileId,
}: SelectorDisplayNameArgs) {
const detailId = typeof value === 'string' && value.length > 0 ? value : undefined
@@ -43,6 +57,13 @@ export function useSelectorDisplayName({
planId,
teamId,
knowledgeBaseId,
baseId,
datasetId,
serviceDeskId,
siteId,
collectionId,
spreadsheetId,
fileId,
})
}, [
subBlock,
@@ -54,6 +75,13 @@ export function useSelectorDisplayName({
planId,
teamId,
knowledgeBaseId,
baseId,
datasetId,
serviceDeskId,
siteId,
collectionId,
spreadsheetId,
fileId,
])
const key = resolution?.key
@@ -553,6 +553,51 @@ export function validateMicrosoftGraphId(
return { isValid: true, sanitized: value }
}
/**
* Validates SharePoint site IDs used in Microsoft Graph API.
*
* Site IDs are compound identifiers: `hostname,spsite-guid,spweb-guid`
* (e.g. `contoso.sharepoint.com,2C712604-1370-44E7-A1F5-426573FDA80A,2D2244C3-251A-49EA-93A8-39E1C3A060FE`).
* The API also accepts partial forms like a single GUID or just a hostname.
*
* Allowed characters: alphanumeric, periods, hyphens, and commas.
*
* @param value - The SharePoint site ID to validate
* @param paramName - Name of the parameter for error messages
* @returns ValidationResult
*/
export function validateSharePointSiteId(
value: string | null | undefined,
paramName = 'siteId'
): ValidationResult {
if (value === null || value === undefined || value === '') {
return {
isValid: false,
error: `${paramName} is required`,
}
}
if (value.length > 512) {
return {
isValid: false,
error: `${paramName} exceeds maximum length`,
}
}
if (!/^[a-zA-Z0-9.\-,]+$/.test(value)) {
logger.warn('Invalid characters in SharePoint site ID', {
paramName,
value: value.substring(0, 100),
})
return {
isValid: false,
error: `${paramName} contains invalid characters`,
}
}
return { isValid: true, sanitized: value }
}
/**
* Validates Jira Cloud IDs (typically UUID format)
*
@@ -52,6 +52,9 @@ interface ExtendedSelectorContext {
siteId?: string
collectionId?: string
spreadsheetId?: string
baseId?: string
datasetId?: string
serviceDeskId?: string
}
function getSemanticFallback(subBlockId: string, subBlockConfig?: SubBlockConfig): string {
@@ -163,6 +166,9 @@ async function resolveSelectorValue(
siteId: extendedContext.siteId,
collectionId: extendedContext.collectionId,
spreadsheetId: extendedContext.spreadsheetId,
baseId: extendedContext.baseId,
datasetId: extendedContext.datasetId,
serviceDeskId: extendedContext.serviceDeskId,
}
if (definition.fetchById) {
@@ -240,6 +246,9 @@ function extractExtendedContext(
siteId: getStringValue('siteId'),
collectionId: getStringValue('collectionId'),
spreadsheetId: getStringValue('spreadsheetId') || getStringValue('fileId'),
baseId: getStringValue('baseId') || getStringValue('baseSelector'),
datasetId: getStringValue('datasetId') || getStringValue('datasetSelector'),
serviceDeskId: getStringValue('serviceDeskId') || getStringValue('serviceDeskSelector'),
}
}
@@ -313,6 +322,9 @@ export async function resolveValueForDisplay(
siteId: extendedContext.siteId,
collectionId: extendedContext.collectionId,
spreadsheetId: extendedContext.spreadsheetId,
baseId: extendedContext.baseId,
datasetId: extendedContext.datasetId,
serviceDeskId: extendedContext.serviceDeskId,
})
if (resolution?.key) {