Skip to content

Commit e4166ec

Browse files
committed
fix(docs): keep hidden params the block mapper supplies
The carve-out only recognized an identity match between a subBlock id and a tool param, so a block that renames or assembles the value in tools.config.params was invisible to it -- and the row was dropped even though the user types it. Cal.com's attendee (required) is assembled from attendeeName/attendeeEmail/ attendeeTimeZone; JSM's workspaceId comes from assetWorkspaceId; Textract writes parameters.file from a field whose canonicalParamId is 'document', which left the Mistral PDF Parser documenting zero inputs. Collects params written by any accumulator identifier, not just 'result', since the two real mappers use different names. Object keys are collected without proving they are top-level, so a nested key can produce a false keep -- one hard-to-set row is better than hiding a required input.
1 parent d91ae51 commit e4166ec

7 files changed

Lines changed: 217 additions & 2 deletions

File tree

apps/docs/content/docs/en/integrations/calcom.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ Create a new booking on Cal.com
4444
| --------- | ---- | -------- | ----------- |
4545
| `eventTypeId` | number | Yes | The ID of the event type to book |
4646
| `start` | string | Yes | Start time in UTC ISO 8601 format \(e.g., 2024-01-15T09:00:00Z\) |
47+
| `attendee` | object | Yes | Attendee information object with name, email, timeZone, and optional phoneNumber \(constructed from individual attendee fields\) |
4748
| `guests` | array | No | Array of guest email addresses |
4849
| `lengthInMinutes` | number | No | Duration of the booking in minutes \(overrides event type default\) |
4950
| `metadata` | object | No | Custom metadata to attach to the booking |

apps/docs/content/docs/en/integrations/google_drive.mdx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ List files and folders in Google Drive with complete metadata
4343
| Parameter | Type | Required | Description |
4444
| --------- | ---- | -------- | ----------- |
4545
| `folderSelector` | string | No | Google Drive folder ID to list files from \(e.g., 1ABCxyz...\) |
46+
| `folderId` | string | No | The ID of the folder to list files from \(internal use\) |
4647
| `query` | string | No | Search term to filter files by name \(e.g. "budget" finds files with "budget" in the name\). Do NOT use Google Drive query syntax here - just provide a plain search term. |
4748
| `pageSize` | number | No | The maximum number of files to return \(default: 100\) |
4849

@@ -226,6 +227,7 @@ Create a new folder in Google Drive with complete metadata returned
226227
| --------- | ---- | -------- | ----------- |
227228
| `fileName` | string | Yes | Name of the folder to create |
228229
| `folderSelector` | string | No | Google Drive parent folder ID to create the folder in \(e.g., 1ABCxyz...\) |
230+
| `folderId` | string | No | ID of the parent folder \(internal use\) |
229231

230232
#### Output
231233

@@ -283,6 +285,7 @@ Upload a file to Google Drive with complete metadata returned
283285
| `content` | string | No | Text content to upload \(use this OR file, not both\) |
284286
| `mimeType` | string | No | The MIME type of the file to upload \(auto-detected from file if not provided\) |
285287
| `folderSelector` | string | No | Google Drive folder ID to upload the file to \(e.g., 1ABCxyz...\) |
288+
| `folderId` | string | No | The ID of the folder to upload the file to \(internal use\) |
286289

287290
#### Output
288291

apps/docs/content/docs/en/integrations/jira_service_management.mdx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -963,6 +963,7 @@ List Assets (Insight/CMDB) object schemas in Jira Service Management
963963
| Parameter | Type | Required | Description |
964964
| --------- | ---- | -------- | ----------- |
965965
| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
966+
| `workspaceId` | string | No | Assets workspace ID \(resolved automatically when omitted\) |
966967
| `startAt` | number | No | Pagination start index \(e.g., 0, 50\) |
967968
| `maxResults` | number | No | Maximum schemas to return \(e.g., 25, 50\) |
968969
| `includeCounts` | boolean | No | Include object and object-type counts per schema |
@@ -992,6 +993,7 @@ Get a single Assets (Insight/CMDB) object schema by ID
992993
| Parameter | Type | Required | Description |
993994
| --------- | ---- | -------- | ----------- |
994995
| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
996+
| `workspaceId` | string | No | Assets workspace ID \(resolved automatically when omitted\) |
995997
| `schemaId` | string | Yes | The Assets object schema ID |
996998

997999
#### Output
@@ -1017,6 +1019,7 @@ List object types within an Assets (Insight/CMDB) object schema
10171019
| Parameter | Type | Required | Description |
10181020
| --------- | ---- | -------- | ----------- |
10191021
| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
1022+
| `workspaceId` | string | No | Assets workspace ID \(resolved automatically when omitted\) |
10201023
| `schemaId` | string | Yes | The Assets object schema ID to list object types for |
10211024
| `excludeAbstract` | boolean | No | Exclude abstract object types from the result |
10221025

@@ -1044,6 +1047,7 @@ Get the attribute definitions for an Assets (Insight/CMDB) object type. Use the
10441047
| Parameter | Type | Required | Description |
10451048
| --------- | ---- | -------- | ----------- |
10461049
| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
1050+
| `workspaceId` | string | No | Assets workspace ID \(resolved automatically when omitted\) |
10471051
| `objectTypeId` | string | Yes | The Assets object type ID |
10481052
| `onlyValueEditable` | boolean | No | Return only attributes whose values can be edited |
10491053
| `query` | string | No | Filter attributes by a search query |
@@ -1074,6 +1078,7 @@ Search Assets (Insight/CMDB) objects using AQL (Assets Query Language), e.g. obj
10741078
| Parameter | Type | Required | Description |
10751079
| --------- | ---- | -------- | ----------- |
10761080
| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
1081+
| `workspaceId` | string | No | Assets workspace ID \(resolved automatically when omitted\) |
10771082
| `qlQuery` | string | Yes | AQL query string \(e.g., objectType = "Host" AND "Operating System" = "Ubuntu"\) |
10781083
| `page` | number | No | Page number \(1-based, defaults to 1\) |
10791084
| `resultsPerPage` | number | No | Results per page \(e.g., 25, 50\) |
@@ -1105,6 +1110,7 @@ Get a single Assets (Insight/CMDB) object by ID, including its attribute values
11051110
| Parameter | Type | Required | Description |
11061111
| --------- | ---- | -------- | ----------- |
11071112
| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
1113+
| `workspaceId` | string | No | Assets workspace ID \(resolved automatically when omitted\) |
11081114
| `objectId` | string | Yes | The Assets object ID |
11091115

11101116
#### Output
@@ -1133,6 +1139,7 @@ Create an Assets (Insight/CMDB) object of a given object type. Attributes use ob
11331139
| Parameter | Type | Required | Description |
11341140
| --------- | ---- | -------- | ----------- |
11351141
| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
1142+
| `workspaceId` | string | No | Assets workspace ID \(resolved automatically when omitted\) |
11361143
| `objectTypeId` | string | Yes | The object type ID to create the object under |
11371144
| `attributes` | json | Yes | Array of attributes: \[\{ objectTypeAttributeId, objectAttributeValues: \[\{ value \}\] \}\] |
11381145

@@ -1162,6 +1169,7 @@ Update an existing Assets (Insight/CMDB) object. Provide the attributes to chang
11621169
| Parameter | Type | Required | Description |
11631170
| --------- | ---- | -------- | ----------- |
11641171
| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
1172+
| `workspaceId` | string | No | Assets workspace ID \(resolved automatically when omitted\) |
11651173
| `objectId` | string | Yes | The Assets object ID to update |
11661174
| `attributes` | json | Yes | Array of attributes to set: \[\{ objectTypeAttributeId, objectAttributeValues: \[\{ value \}\] \}\] |
11671175
| `objectTypeId` | string | No | Optional object type ID \(only if changing the type\) |
@@ -1192,6 +1200,7 @@ Delete an Assets (Insight/CMDB) object by ID
11921200
| Parameter | Type | Required | Description |
11931201
| --------- | ---- | -------- | ----------- |
11941202
| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
1203+
| `workspaceId` | string | No | Assets workspace ID \(resolved automatically when omitted\) |
11951204
| `objectId` | string | Yes | The Assets object ID to delete |
11961205

11971206
#### Output

apps/docs/content/docs/en/integrations/mistral_parse.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ Integrate Mistral Parse into the workflow. Can extract text from uploaded PDF do
3939

4040
| Parameter | Type | Required | Description |
4141
| --------- | ---- | -------- | ----------- |
42+
| `file` | file | Yes | Normalized UserFile from file upload or file reference |
4243

4344
#### Output
4445

apps/docs/content/docs/en/integrations/textract.mdx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ Integrate AWS Textract into your workflow to extract text, tables, forms, and ke
3939

4040
| Parameter | Type | Required | Description |
4141
| --------- | ---- | -------- | ----------- |
42+
| `file` | file | No | Document to be processed \(JPEG, PNG, or single-page PDF\). |
4243

4344
#### Output
4445

@@ -89,6 +90,7 @@ Extract structured invoice and receipt fields using AWS Textract AnalyzeExpense
8990
| `secretAccessKey` | string | Yes | AWS Secret Access Key |
9091
| `region` | string | Yes | AWS region for Textract service \(e.g., us-east-1\) |
9192
| `processingMode` | string | No | Document type: single-page or multi-page. Defaults to single-page. |
93+
| `file` | file | No | Invoice or receipt to be processed \(JPEG, PNG, or single-page PDF\). |
9294
| `filePath` | string | No | URL to an invoice or receipt to be processed, if not uploaded directly. |
9395
| `s3Uri` | string | No | S3 URI for multi-page processing \(s3://bucket/key\). |
9496

@@ -118,7 +120,10 @@ Extract identity document fields using AWS Textract AnalyzeID
118120
| `accessKeyId` | string | Yes | AWS Access Key ID |
119121
| `secretAccessKey` | string | Yes | AWS Secret Access Key |
120122
| `region` | string | Yes | AWS region for Textract service \(e.g., us-east-1\) |
123+
| `file` | file | No | Front of the identity document \(JPEG, PNG, or PDF\). |
121124
| `filePath` | string | No | URL to the front of the identity document, if not uploaded directly. |
125+
| `fileBack` | file | No | Back of the identity document, if applicable \(JPEG, PNG, or PDF\). |
126+
| `filePathBack` | string | No | URL to the back of the identity document, if not uploaded directly. |
122127

123128
#### Output
124129

scripts/generate-docs.test.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import fs from 'fs'
22
import path from 'path'
33
import { describe, expect, it } from 'vitest'
44
import {
5+
extractBlockSuppliedParamIds,
56
extractUserSettableParamIds,
67
getToolInfo,
78
parseConstProperties,
@@ -165,3 +166,85 @@ describe('subBlock param extraction', () => {
165166
)
166167
})
167168
})
169+
170+
describe('hidden params supplied by the block mapper', () => {
171+
const blockSource = (blockFile: string) =>
172+
fs.readFileSync(path.join(import.meta.dirname, '../apps/sim/blocks/blocks', blockFile), 'utf-8')
173+
174+
const paramNames = async (toolId: string, blockFile: string) => {
175+
const info = await getToolInfo(toolId, extractBlockSuppliedParamIds(blockSource(blockFile)))
176+
return info?.params.map((param) => param.name) ?? []
177+
}
178+
179+
it("keeps Cal.com's required attendee, assembled as result.attendee in the mapper", async () => {
180+
expect(extractBlockSuppliedParamIds(blockSource('calcom.ts'))).toContain('attendee')
181+
await expect(paramNames('calcom_create_booking', 'calcom.ts')).resolves.toContain('attendee')
182+
})
183+
184+
it("keeps JSM's workspaceId, renamed from assetWorkspaceId in the mapper", async () => {
185+
expect(extractBlockSuppliedParamIds(blockSource('jira_service_management.ts'))).toContain(
186+
'workspaceId'
187+
)
188+
await expect(
189+
paramNames('jsm_list_object_schemas', 'jira_service_management.ts')
190+
).resolves.toContain('workspaceId')
191+
})
192+
193+
it('keeps the file params Textract renames from its document field', async () => {
194+
const ids = extractBlockSuppliedParamIds(blockSource('textract.ts'))
195+
expect(ids).toContain('file')
196+
expect(ids).toContain('fileBack')
197+
expect(ids).toContain('filePathBack')
198+
199+
const params = await paramNames('textract_analyze_id', 'textract.ts')
200+
expect(params).toContain('file')
201+
expect(params).toContain('fileBack')
202+
expect(params).toContain('filePathBack')
203+
})
204+
205+
it('keeps the Mistral parser file param, so its Input table is not empty', async () => {
206+
await expect(paramNames('mistral_parser_v3', 'mistral_parse.ts')).resolves.toContain('file')
207+
})
208+
209+
it('still drops resolver-derived hidden params with no user surface', async () => {
210+
await expect(paramNames('jira_retrieve', 'jira.ts')).resolves.not.toContain('cloudId')
211+
await expect(
212+
paramNames('jsm_list_object_schemas', 'jira_service_management.ts')
213+
).resolves.not.toContain('cloudId')
214+
215+
const salesforce = await paramNames('salesforce_query', 'salesforce.ts')
216+
expect(salesforce).not.toContain('idToken')
217+
expect(salesforce).not.toContain('instanceUrl')
218+
219+
await expect(paramNames('netsuite_execute_suiteql', 'netsuite.ts')).resolves.not.toContain(
220+
'instanceUrl'
221+
)
222+
await expect(paramNames('snowflake_execute_sql', 'snowflake.ts')).resolves.not.toContain(
223+
'domain'
224+
)
225+
await expect(paramNames('pipedrive_get_deal', 'pipedrive.ts')).resolves.not.toContain(
226+
'authStyle'
227+
)
228+
await expect(paramNames('zoho_desk_list_tickets', 'zoho-desk.ts')).resolves.not.toContain(
229+
'apiDomain'
230+
)
231+
})
232+
233+
it('ignores a commented-out mapper assignment', () => {
234+
const ids = extractBlockSuppliedParamIds(`
235+
subBlocks: [{ id: 'operation' }],
236+
tools: {
237+
config: {
238+
params: (params) => {
239+
const result: Record<string, unknown> = {}
240+
// result.commentedOut = params.nope
241+
result.realOne = params.yes
242+
return result
243+
},
244+
},
245+
},
246+
`)
247+
expect(ids).toContain('realOne')
248+
expect(ids).not.toContain('commentedOut')
249+
})
250+
})

scripts/generate-docs.ts

Lines changed: 115 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,10 @@ interface BlockConfig {
200200
access?: string[]
201201
}
202202
operations?: OperationInfo[]
203-
/** Param names the block exposes to the user via its own `subBlocks` (id or `canonicalParamId`). */
203+
/**
204+
* Param names the block itself supplies — via a `subBlocks` field (id or
205+
* `canonicalParamId`) or via its `tools.config.params` mapper.
206+
*/
204207
userSettableParamIds?: string[]
205208
docsLink?: string
206209
[key: string]: any
@@ -735,6 +738,116 @@ export function extractUserSettableParamIds(blockContent: string, blockName = 'b
735738
return [...ids]
736739
}
737740

741+
/**
742+
* Locates the bodies of every `tools.config.params` mapper in `scannable`.
743+
*
744+
* Returns `[start, end)` index pairs into `scannable` (a length-preserving blanked copy, so
745+
* the same indices address the original content). A file can hold several block configs
746+
* (Textract ships a v1 and a v2 block), so every `tools` object is scanned rather than only
747+
* the first. Handles both `params: (params) => { ... }` and the concise
748+
* `params: (params) => ({ ... })` form.
749+
*/
750+
function findMapperBodyRanges(scannable: string): [number, number][] {
751+
const ranges: [number, number][] = []
752+
const toolsRegex = /\btools\s*:\s*\{/g
753+
let toolsMatch: RegExpExecArray | null
754+
755+
while ((toolsMatch = toolsRegex.exec(scannable)) !== null) {
756+
const toolsEnd = findMatchingClose(scannable, toolsMatch.index + toolsMatch[0].length - 1)
757+
if (toolsEnd === -1) continue
758+
toolsRegex.lastIndex = toolsEnd
759+
760+
const toolsRegion = scannable.slice(toolsMatch.index, toolsEnd)
761+
const configMatch = /\bconfig\s*:\s*\{/.exec(toolsRegion)
762+
if (!configMatch) continue
763+
const configStart = toolsMatch.index + configMatch.index + configMatch[0].length - 1
764+
const configEnd = findMatchingClose(scannable, configStart)
765+
if (configEnd === -1) continue
766+
767+
const configRegion = scannable.slice(configStart, configEnd)
768+
const paramsMatch = /\bparams\s*(?::\s*)?\(/.exec(configRegion)
769+
if (!paramsMatch) continue
770+
const argsStart = configStart + paramsMatch.index + paramsMatch[0].length - 1
771+
const argsEnd = findMatchingClose(scannable, argsStart, '(', ')')
772+
if (argsEnd === -1) continue
773+
774+
const afterArgs = scannable.slice(argsEnd, configEnd)
775+
const bodyMatch = /^\s*(?::[^=({]*)?(?:=>\s*)?([({])/.exec(afterArgs)
776+
if (!bodyMatch) continue
777+
const open = bodyMatch[1] as '(' | '{'
778+
const bodyStart = argsEnd + bodyMatch[0].length - 1
779+
const bodyEnd = findMatchingClose(scannable, bodyStart, open, open === '(' ? ')' : '}')
780+
if (bodyEnd === -1) continue
781+
ranges.push([bodyStart, bodyEnd])
782+
}
783+
784+
return ranges
785+
}
786+
787+
/**
788+
* Collects the tool-param names a block's own `tools.config.params` mapper writes.
789+
*
790+
* A block can supply a hidden tool param without ever declaring a subBlock of that name:
791+
* Cal.com assembles `result.attendee` from `attendeeName`/`attendeeEmail`/`attendeeTimeZone`,
792+
* JSM renames its `assetWorkspaceId` field to `workspaceId`, and Textract renames its
793+
* `document` field to `file`. All are user-driven and must stay documented, so this scan
794+
* catches both shapes — `<anyIdentifier>.<param> = …` assignments (the accumulator is named
795+
* `result` in one block and `parameters` in another, so the name is never assumed) and
796+
* `<param>:` keys of the objects the mapper returns.
797+
*
798+
* The rule is deliberately biased toward keeping. Object keys are collected without proving
799+
* they are top-level params, so a key on a nested object (Cal.com's `attendee.name`) can keep
800+
* a same-named hidden param that the mapper never actually supplies. That false keep costs a
801+
* reader one hard-to-set row; a false drop hides a required input — the exact failure this
802+
* filter exists to prevent, and one it has already caused. When the two are in tension, keep.
803+
*
804+
* Scanning runs on the blanked copy, so a commented-out or string-embedded mapper cannot
805+
* contribute names.
806+
*/
807+
export function extractMapperWrittenParamIds(blockContent: string): string[] {
808+
const scannable = blankStringsAndComments(blockContent)
809+
const ids = new Set<string>()
810+
811+
for (const [start, end] of findMapperBodyRanges(scannable)) {
812+
const body = scannable.slice(start, end)
813+
814+
const assignmentRegex = /(?:^|[^.\w$])[A-Za-z_$][\w$]*\.([A-Za-z_$][\w$]*)\s*=(?!=)/g
815+
let match: RegExpExecArray | null
816+
while ((match = assignmentRegex.exec(body)) !== null) ids.add(match[1])
817+
818+
const keyRegex = /(?:^|[^?.\w$])([A-Za-z_$][\w$]*)\s*:/g
819+
while ((match = keyRegex.exec(body)) !== null) ids.add(match[1])
820+
821+
/**
822+
* Quoted keys survive blanking only as their delimiters, so the name is read back from
823+
* the original content at the same index — `blankStringsAndComments` is length-preserving.
824+
*/
825+
const quotedKeyRegex = /(['"])[^'"\n]*\1\s*:/g
826+
while ((match = quotedKeyRegex.exec(body)) !== null) {
827+
const keyStart = start + match.index
828+
const original = blockContent.slice(keyStart, keyStart + match[0].length)
829+
const name = /(['"])([A-Za-z_$][\w$]*)\1/.exec(original)?.[2]
830+
if (name) ids.add(name)
831+
}
832+
}
833+
834+
return [...ids]
835+
}
836+
837+
/**
838+
* Every param name the block itself supplies — via a user-facing `subBlocks` field or via
839+
* its `tools.config.params` mapper. A hidden tool param in this set stays in the public
840+
* Input table; the rest are genuinely resolver-derived and stay filtered out.
841+
*/
842+
export function extractBlockSuppliedParamIds(blockContent: string, blockName = 'block'): string[] {
843+
return [
844+
...new Set([
845+
...extractUserSettableParamIds(blockContent, blockName),
846+
...extractMapperWrittenParamIds(blockContent),
847+
]),
848+
]
849+
}
850+
738851
/**
739852
* Extract operation options from the subBlock with id: 'operation' (if present).
740853
* Returns { label, id } pairs — label is the display name, id is the option's id field
@@ -1435,7 +1548,7 @@ function extractBlockConfigFromContent(
14351548
const triggerIds = extractTriggersAvailable(blockContent, fileContent)
14361549
let ownSettableParamIds: string[] = []
14371550
try {
1438-
ownSettableParamIds = extractUserSettableParamIds(blockContent, blockName)
1551+
ownSettableParamIds = extractBlockSuppliedParamIds(blockContent, blockName)
14391552
} catch (error) {
14401553
/**
14411554
* Recorded rather than rethrown so one unreadable block cannot drop itself from the

0 commit comments

Comments
 (0)