poc: runtime resource override resolution for getByName#575
Conversation
Adds override resolution to FolderScopedService.getByNameLookup so that
admin-time resource overrides (injected into index.html as
<script id="uipath-overrides">) are applied at runtime before making
API calls.
When a developer calls assets.getByName("CustomerConfig"), the compiled
bundle has "CustomerConfig" hardcoded. At runtime, the SDK now reads the
override dict from the DOM. If an admin overrode "CustomerConfig" to
"ProdConfig", the $filter sent to the API uses "ProdConfig" instead.
This is a POC — production implementation needs:
- Cache invalidation when overrides change
- Support for entity/bucket services (not just folder-scoped)
- Integration with the overrides team's delivery mechanism
- Unit tests
Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
|
|
||
| try { | ||
| cachedOverrides = JSON.parse(el.textContent ?? '{}'); | ||
| console.log('[UiPath SDK] Loaded overrides:', cachedOverrides); |
There was a problem hiding this comment.
console.log calls must be removed from production code. The PR description lists "Remove console.log statements" as a TODO, but these statements are still present on lines 36, 39, and 62. They will emit SDK internals to the browser console for every app using this feature and should be removed before merging.
| function loadOverrides(): OverrideDict { | ||
| if (loaded) return cachedOverrides ?? {}; | ||
|
|
||
| loaded = true; |
There was a problem hiding this comment.
loaded = true is set before the element check at line 32. If this module initialises before <script id="uipath-overrides"> is in the DOM (e.g., a service is instantiated during app startup, before the injected script is parsed), loaded is permanently flipped to true and every subsequent call returns {}. The override system silently stops working for the entire session with no error.
Consider only setting loaded = true (and populating cachedOverrides) when the element is actually found and parsed successfully, or at minimum documenting that callers must not invoke any getByName* method until the DOM is fully loaded.
| throw new NotFoundError({ | ||
| message: `${resourceType} '${validatedName}' not found${folderHint}.`, | ||
| message: `${resourceType} '${resolvedName}' not found${folderHint}.`, | ||
| }); |
There was a problem hiding this comment.
The not-found error message now exposes resolvedName (the admin-override value, e.g. "ProdConfig") to the developer. The developer wrote getByName("CustomerConfig") — they have no knowledge of "ProdConfig" and can't act on an error about it.
Consider including the original name to make the error actionable:
message: `${resourceType} '${validatedName}'${override ? ` (overridden to '${resolvedName}')` : ''} not found${folderHint}.`,This way users with overrides configured get a diagnostic hint, while users without overrides see the same clean message as before.
Review summaryThree issues found (all new — no prior threads to dedupe against): 1. Logging calls in production code (resolve-override.ts lines 36, 39, 62) 2. Silent permanent failure if module loads before DOM is ready (resolve-override.ts line 27) 3. Not-found error exposes admin override name (folder-scoped.ts ~line 139) |
| let cachedOverrides: OverrideDict | null = null; | ||
| let loaded = false; |
There was a problem hiding this comment.
Module-level mutable state will cause test pollution in vitest. Because vitest runs tests in the same process with a shared module registry, loaded and cachedOverrides persist across test files — once any test triggers loadOverrides(), the cached value is frozen for the rest of the suite unless vi.resetModules() is called explicitly.
Consider exporting a resetOverridesCache() function (for tests only, marked @internal) or refactoring to a factory/class so tests can create a fresh instance per test:
| let cachedOverrides: OverrideDict | null = null; | |
| let loaded = false; | |
| export interface ResourceOverride { | |
| name: string; | |
| folderPath: string; | |
| } | |
| type OverrideDict = Record<string, ResourceOverride>; | |
| let cachedOverrides: OverrideDict | null = null; | |
| let loaded = false; | |
| /** @internal — test helper only */ | |
| export function _resetOverridesCache(): void { | |
| cachedOverrides = null; | |
| loaded = false; | |
| } |
| folderPath: string, | ||
| ): ResourceOverride | null { | ||
| const overrides = loadOverrides(); | ||
| const key = `${resourceType}.${name}.${folderPath}`; |
There was a problem hiding this comment.
The dot-separated key format is ambiguous whenever a resource name or folder path contains a .. For example, an asset named "v2.0" in folder "Shared/Apps" produces asset.v2.0.Shared/Apps, which can't be unambiguously split back into its three parts.
Use a separator that cannot appear in resource names or folder paths, e.g. :::
| const key = `${resourceType}.${name}.${folderPath}`; | |
| const key = `${resourceType}::${name}::${folderPath}`; |
The generation side (syncResourceOverwritesToCdn) must use the same separator.
| const override: ResourceOverride | null = resolveOverride( | ||
| resourceType.toLowerCase(), | ||
| validatedName, | ||
| folderPath ?? '.' |
There was a problem hiding this comment.
Using '.' as the fallback for an undefined folderPath creates a silent coupling with the override generation side — both sides must agree on the same sentinel value, or lookups will always miss when no explicit folder is passed. If the generation side uses a different default (empty string, undefined, or some other sentinel), overrides will silently not apply.
Either document the contract explicitly in the JSDoc of resolveOverride, or pass undefined / '' as-is and let the generation side handle the undefined case consistently:
| folderPath ?? '.' | |
| folderPath ?? '' |
| const folderHint = describeFolderForError(folderId, folderKey, resolvedFolderPath); | ||
| throw new NotFoundError({ | ||
| message: `${resourceType} '${validatedName}' not found${folderHint}.`, | ||
| message: `${resourceType} '${resolvedName}' not found${folderHint}.`, |
There was a problem hiding this comment.
Using resolvedName in the error message makes debugging harder: a developer who wrote assets.getByName("CustomerConfig") will see Asset 'ProdConfig' not found in their logs and have no idea why, since "ProdConfig" never appears in their code.
When an override is active, include both names in the message:
| message: `${resourceType} '${resolvedName}' not found${folderHint}.`, | |
| message: override | |
| ? `${resourceType} '${validatedName}' (overridden to '${resolvedName}') not found${folderHint}.` | |
| : `${resourceType} '${validatedName}' not found${folderHint}.`, |
Review notesFour issues found (not duplicating the known TODOs already listed in the PR description):
|
|


Summary
POC for runtime resource override resolution in the SDK. Proves that admin-time overrides (injected into
index.htmlat deploy time) can modify resource names used in API calls — even though the original names are hardcoded in the compiled JS bundle.What this does
resolveOverride()utility that reads<script id="uipath-overrides">from the DOM at runtimeFolderScopedService.getByNameLookup()— the shared method used byassets.getByName(),buckets.getByName(),processes.getByName()"CustomerConfig"→ SDK looks up override → API call uses"ProdConfig"insteadHow overrides flow
assets.getByName(bindings.taggedAsset.name)→ compiled toassets.getByName("CustomerConfig")syncResourceOverwritesToCdninjects<script id="uipath-overrides">intoindex.htmlgetByNameLookupcallsresolveOverride("asset", "CustomerConfig", "Shared/PublicApps")$filter=Name eq 'ProdConfig'sent to API (NOT "CustomerConfig")POC validation
Tested with a Vite React app (
npm run build→npm run preview):"CustomerConfig"is hardcoded in the compiled JS bundle (grepconfirms)alpha.uipath.com/appsdev/appsdevDefaultContext
This is part of the Resource Access Policy (RAP) design for public coded apps. RAP needs to know the actual resource names sent in API calls to validate them at the Cloudflare Worker edge. The override resolution ensures the policy uses the same names the SDK sends.
Not production-ready — needs:
Generated with Claude Code