feat(functions): add Functions service for invoking coded functions [PLT-106037] - #614
feat(functions): add Functions service for invoking coded functions [PLT-106037]#614amrit-agarwal-1 wants to merge 5 commits into
Conversation
…PLT-106037] Adds a modular `@uipath/uipath-typescript/functions` service that discovers and invokes JS/Python coded functions, masking the underlying process + HTTP trigger plumbing. - getAll(options?): folder-scoped, paginated listing over /odata/HttpTriggers - invoke(name, input?, options?): resolves the function by name, resolves the folder key, and calls the function's HTTP trigger; returns typed output - bound invoke() on each getAll item Folder context accepted as folderId / folderKey / folderPath (or SDK-init folder context). Verified end-to-end against a live tenant. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…on [PLT-106037] - invoke() now follows the gateway's 303 status long-poll chain for functions exceeding the ~25s response window: explicit redirect handling, fresh auth token per poll leg, bounded by a new maxWaitSeconds option (default 300) with a clear timeout error. Browsers fall back to the engine's automatic redirect handling (manual redirects are not readable there). - New jobKey option on invoke: sent as X-UIPATH-JobKey so the run is recorded with the parent job's key (parentJobKey) and inherits its context and licensing transaction. Applied to the invocation leg only. - ApiClient: RequestSpec.redirect passthrough and a raw response type so the invoke leg can observe the 303 chain. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…okup errors [PLT-106037] Coded functions over HTTP are request/response only, so the long-running handling is removed: no platform branch, no manual 303 loop, no job polling. invoke() issues one request and returns the output, letting the engine follow any redirect. Drops the maxWaitSeconds option. Also: - filter on processName / processSlug now maps to the Release navigation path, so functions can be narrowed to one package server-side - a name lookup miss lists the folder's function names, since the usual cause is passing a package name - ApiClient no longer sends Content-Type on GET/HEAD, which Orchestrator rejects for GET function invocations Co-Authored-By: Claude <noreply@anthropic.com>
Drops machinery left behind by the abandoned 303 long-poll invoke approach and
tightens the surface the PR adds, so the diff reflects only what the feature needs.
Shared core (reverts unused additions to the HTTP layer):
- RESPONSE_TYPES.RAW, the 'raw' member of ResponseType, and the RAW passthrough
branch in ApiClient — nothing ever set responseType 'raw'. The platform
long-polls the job server-side and returns the output as the response body, so
no caller drives a redirect chain.
- RequestSpec.redirect and its `redirect: options.redirect` fetch passthrough —
no call site ever set it.
Functions service:
- release.id in RawFunctionTrigger: declared but never read, contradicting the
type's own "only the fields consumed" contract.
- fn.method.toUpperCase() at the invoke call site: BaseService.request() already
uppercases the verb.
- Private helpers took `options?` and defended with `options ?? {}` / `options?.x`
while their only call site always passes a defined object; narrowed to a
required FolderScopedOptions.
- The "and N more" suffix on the name-lookup error could only ever render
"and 1 more" ($top caps the response one row past the limit), so it now says
"and more".
- resourceType label 'Function.getByName' named a method the service does not
expose; now 'Functions.invoke', matching the convention used elsewhere.
- The subpath barrel exported the class under both Functions and FunctionService;
only the public alias is exported now, matching Notifications and Governance.
The unit test imports the class from the implementation file instead.
Docs:
- Every FunctionServiceModel @example called methods on `fns`, a variable no
example declares (the Usage block declares `functions`). Examples now use
`functions` throughout, with the getAll result renamed to `deployed` where it
would otherwise shadow it.
- The module @example used a literal folderId; now the <folderId> placeholder.
Tests:
- Deleted functions.browser.test.ts. Its only distinguishing assertion was
`expect.not.objectContaining({ redirect: 'manual' })` against the now-removed
field, and the service has no platform branch, so the rest duplicated
functions.test.ts.
- Removed the unused ERROR_FUNCTION_NOT_FOUND test constant.
Also drops an unrelated "AI App Builders" mkdocs nav section that leaked into
this branch; it points at pages that live on the docs/ai-app-builders branch and
would have published five dead nav entries.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
4cba22f to
b2eab8c
Compare
|
| * `processSlug`, or `processKey` to narrow the result to a single package. | ||
| * | ||
| * @param options - Query options including folder scoping (`folderId` / `folderKey` / `folderPath`), filtering, and pagination options | ||
| * @returns Promise resolving to either an array of functions {@link NonPaginatedResponse}<{@link FunctionGetResponse}> or a {@link PaginatedResponse}<{@link FunctionGetResponse}> when pagination options are used. |
There was a problem hiding this comment.
Orphaned {@link} tag — this line renders as stray text (or a dangling link) in TypeDoc output. It appears to be a copy-paste artifact; the @returns description on the line above already includes the two relevant links. Remove this line.
| * @returns Promise resolving to either an array of functions {@link NonPaginatedResponse}<{@link FunctionGetResponse}> or a {@link PaginatedResponse}<{@link FunctionGetResponse}> when pagination options are used. |
| getAll: vi.fn(), | ||
| invoke: vi.fn(), | ||
| }; | ||
| }); |
There was a problem hiding this comment.
Convention requires afterEach(() => { vi.clearAllMocks(); }) even when mocks are recreated in beforeEach. The service test file includes this; the model test should too for consistency.
| }); | |
| }); | |
| afterEach(() => { | |
| vi.clearAllMocks(); | |
| }); |
Review findingsThree minor convention violations flagged:
Everything else looks correct: |
…06037]
- functions.test.ts called vi.unstubAllGlobals() in afterEach while stubbing no
globals at all — the repo's only unpaired instance (jobs and
conversational-agent each pair one unstub with 4-8 stubs).
- RawFunctionTrigger.folderName's "(null on list responses)" qualifier could not
discriminate anything: the service only ever reads the HttpTriggers collection,
so every response it sees is a list response.
- Dropped an orphaned {@link} line TypeDoc would render as stray text.
- createMockRawFunctionTrigger typed as Record<string, unknown> instead of any.
- Added the missing afterEach teardown to the model test.
- FunctionMap's comment claimed it mapped responses; it is only used by
transformOptions for OData query rewriting.
Co-Authored-By: Claude <noreply@anthropic.com>
871a7a6 to
12cad94
Compare
|
✅ No issues found. Checked for bugs and CLAUDE.md compliance. |
Method Added
functions.getAll()getAll<T extends FunctionGetAllOptions>(options?: T): Promise<T extends HasPaginationOptions<T> ? PaginatedResponse<FunctionGetResponse> : NonPaginatedResponse<FunctionGetResponse>>functions.invoke()invoke<TInput extends object, TOutput>(name: string, input?: TInput, options?: FunctionInvokeOptions): Promise<TOutput>fn.invoke()invoke<TInput extends object, TOutput>(input?: TInput): Promise<TOutput>Endpoint Called
getAll()orchestrator_/odata/HttpTriggersOR.Defaultinvoke()orchestrator_/odata/HttpTriggers(name lookup)OR.Defaultinvoke()orchestrator_/odata/Folders({id})(folder key resolution)OR.FoldersorOR.Folders.Readinvoke()orchestrator_/t/{folderKey}/{processSlug}/{slug}(HTTP trigger)OR.Default@uipath/uipath-typescript/functions. Internal classFunctionService, public aliasFunctions. ExtendsFolderScopedService.folderId,folderKey, orfolderPath(or resolved from the SDK's init-time folder context), via the sharedresolveFolderHeadersutility.getAll()supports OData pagination ($top,$skip,$count) and filtering; a function maps 1:1 to an HTTP trigger, so listing is done at the trigger level.invoke()is composite (see flow below). It calls the HTTP trigger directly — notProcesses.start()— so HTTP-mode function context is preserved. Invocation is synchronous (the platform long-polls the job and returns the output as the HTTP response body). Functions declared withGetreceive input as query parameters; all other verbs receive it as the JSON body.Example Usage
API Response vs SDK Response
getAll — Transform pipeline
pascalToCamelCaseKeys(row)→ manual reshape toRawFunctionGetResponse(flattens nestedrelease) →createFunctionWithMethods()(attaches boundinvoke)Field mapping (getAll, raw
HttpTriggersrow → SDK)OrganizationUnitIdfolderIdOrganizationUnitFullyQualifiedNamefolderNameReleaseKeyprocessKeyRelease.NameprocessNameRelease.SlugprocessSlugId,Name,Slug,Method,Description,Enabled,InputArguments,EntryPointPathid,name,slug,method,description,enabled,inputArguments,entryPointPathJobPriority,RunAsCaller,CallingMode,MachineRobots, … (~25 job-runner fields)invoke — Composition flow
Internal types (not exported)
RawFunctionTrigger/odata/HttpTriggersrow (camelCased) before reshapeRawFolderResponse/odata/Folders({id})shape — folderKeyonlySample SDK Response
getAll(){ "items": [ { "id": "e758581f-2f78-4d86-a8e9-f4bc3aad52ec", "name": "hello", "slug": "hello", "method": "Post", "description": "Returns a greeting message.", "enabled": true, "inputArguments": "{\"name\":\"World\"}", "entryPointPath": "content/functions/hello.ts", "processKey": "d1519612-2961-488e-af7a-7379cc1c3544", "processName": "my-functions", "processSlug": "my-functions", "folderId": 2843389, "folderName": "Shared/Solution 121 new" } ], "totalCount": 1 }invoke('hello', { name: 'Alice' }, { folderId }){ "message": "Hello, Alice!" }🤖 Auto-generated using onboarding skills