-
Notifications
You must be signed in to change notification settings - Fork 12
poc: runtime resource override resolution for getByName #575
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -7,6 +7,7 @@ import { addPrefixToKeys, transformOptions, FieldMapping } from '../utils/transf | |||||||||
| import { NotFoundError } from '../core/errors'; | ||||||||||
| import { validateName } from '../utils/validation/name-validator'; | ||||||||||
| import { resolveFolderHeaders } from '../utils/folder/folder-headers'; | ||||||||||
| import { resolveOverride, type ResourceOverride } from '../utils/overrides/resolve-override'; | ||||||||||
|
|
||||||||||
| /** | ||||||||||
| * Matches single-quote characters in OData string literals — escaped to `''` | ||||||||||
|
|
@@ -96,10 +97,21 @@ export class FolderScopedService extends BaseService { | |||||||||
| const validatedName = validateName(resourceType, name); | ||||||||||
| const { folderId, folderKey, folderPath, ...queryOptions } = options; | ||||||||||
|
|
||||||||||
| // The compiled code passes the DEFAULT name (e.g. "CustomerConfig"). | ||||||||||
| // At runtime, check if an admin override exists for this resource. | ||||||||||
| // If so, use the overridden name and folderPath for the API call. | ||||||||||
| const override: ResourceOverride | null = resolveOverride( | ||||||||||
| resourceType.toLowerCase(), | ||||||||||
| validatedName, | ||||||||||
| folderPath ?? '.' | ||||||||||
| ); | ||||||||||
| const resolvedName = override?.name ?? validatedName; | ||||||||||
| const resolvedFolderPath = override?.folderPath ?? folderPath; | ||||||||||
|
|
||||||||||
| const headers = resolveFolderHeaders({ | ||||||||||
| folderId, | ||||||||||
| folderKey, | ||||||||||
| folderPath, | ||||||||||
| folderPath: resolvedFolderPath, | ||||||||||
| resourceType: `${resourceType}.getByName`, | ||||||||||
| fallbackFolderKey: this.config.folderKey, | ||||||||||
| }); | ||||||||||
|
|
@@ -110,7 +122,7 @@ export class FolderScopedService extends BaseService { | |||||||||
|
|
||||||||||
| const apiOptions = { | ||||||||||
| ...addPrefixToKeys(apiFieldOptions, ODATA_PREFIX, Object.keys(apiFieldOptions)), | ||||||||||
| '$filter': `Name eq '${validatedName.replace(SINGLE_QUOTE_RE, "''")}'`, | ||||||||||
| '$filter': `Name eq '${resolvedName.replace(SINGLE_QUOTE_RE, "''")}'`, | ||||||||||
| '$top': '1', | ||||||||||
| }; | ||||||||||
|
|
||||||||||
|
|
@@ -121,9 +133,9 @@ export class FolderScopedService extends BaseService { | |||||||||
|
|
||||||||||
| const items = response.data?.value; | ||||||||||
| if (!items?.length) { | ||||||||||
| const folderHint = describeFolderForError(folderId, folderKey, folderPath); | ||||||||||
| const folderHint = describeFolderForError(folderId, folderKey, resolvedFolderPath); | ||||||||||
| throw new NotFoundError({ | ||||||||||
| message: `${resourceType} '${validatedName}' not found${folderHint}.`, | ||||||||||
| message: `${resourceType} '${resolvedName}' not found${folderHint}.`, | ||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using When an override is active, include both names in the message:
Suggested change
|
||||||||||
| }); | ||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The not-found error message now exposes 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. |
||||||||||
| } | ||||||||||
|
|
||||||||||
|
|
||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,66 @@ | ||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||
| * Runtime resource override resolution. | ||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||
| * Reads the override dict from <script type="application/json" id="uipath-overrides"> | ||||||||||||||||||||||||||||||||||||
| * injected into index.html at deploy time (by syncResourceOverwritesToCdn). | ||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||
| * The override dict maps binding keys to new resource names/folders: | ||||||||||||||||||||||||||||||||||||
| * { "asset.CustomerConfig.Shared/PublicApps": { "name": "ProdConfig", "folderPath": "Production/Live" } } | ||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||
| * The DEFAULT name from bindings.gen.ts (compiled into the bundle) acts as | ||||||||||||||||||||||||||||||||||||
| * a LOOKUP KEY. This function resolves it to the overridden value at runtime. | ||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| export interface ResourceOverride { | ||||||||||||||||||||||||||||||||||||
| name: string; | ||||||||||||||||||||||||||||||||||||
| folderPath: string; | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| type OverrideDict = Record<string, ResourceOverride>; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| let cachedOverrides: OverrideDict | null = null; | ||||||||||||||||||||||||||||||||||||
| let loaded = false; | ||||||||||||||||||||||||||||||||||||
|
Comment on lines
+21
to
+22
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Module-level mutable state will cause test pollution in vitest. Because vitest runs tests in the same process with a shared module registry, Consider exporting a
Suggested change
|
||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| function loadOverrides(): OverrideDict { | ||||||||||||||||||||||||||||||||||||
| if (loaded) return cachedOverrides ?? {}; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| loaded = true; | ||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Consider only setting |
||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| if (typeof document === 'undefined') return {}; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| const el = document.getElementById('uipath-overrides'); | ||||||||||||||||||||||||||||||||||||
| if (!el) return {}; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||
| cachedOverrides = JSON.parse(el.textContent ?? '{}'); | ||||||||||||||||||||||||||||||||||||
| console.log('[UiPath SDK] Loaded overrides:', cachedOverrides); | ||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||||||||||||||||||||||||||||||||||||
| return cachedOverrides ?? {}; | ||||||||||||||||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||||||||||||||||
| console.warn('[UiPath SDK] Failed to parse uipath-overrides script tag'); | ||||||||||||||||||||||||||||||||||||
| return {}; | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||
| * Resolves a resource override at runtime. | ||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||
| * @param resourceType - e.g. "asset", "entity", "bucket" | ||||||||||||||||||||||||||||||||||||
| * @param name - the DEFAULT resource name from compiled bindings (e.g. "CustomerConfig") | ||||||||||||||||||||||||||||||||||||
| * @param folderPath - the DEFAULT folder path (e.g. "Shared/PublicApps") | ||||||||||||||||||||||||||||||||||||
| * @returns the override if found, or null if no override exists | ||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||
| export function resolveOverride( | ||||||||||||||||||||||||||||||||||||
| resourceType: string, | ||||||||||||||||||||||||||||||||||||
| name: string, | ||||||||||||||||||||||||||||||||||||
| folderPath: string, | ||||||||||||||||||||||||||||||||||||
| ): ResourceOverride | null { | ||||||||||||||||||||||||||||||||||||
| const overrides = loadOverrides(); | ||||||||||||||||||||||||||||||||||||
| const key = `${resourceType}.${name}.${folderPath}`; | ||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The dot-separated key format is ambiguous whenever a resource name or folder path contains a Use a separator that cannot appear in resource names or folder paths, e.g.
Suggested change
The generation side ( |
||||||||||||||||||||||||||||||||||||
| const override = overrides[key] ?? null; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| if (override) { | ||||||||||||||||||||||||||||||||||||
| console.log(`[UiPath SDK] Override resolved: "${name}" → "${override.name}" (folder: "${override.folderPath}")`); | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| return override; | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using
'.'as the fallback for an undefinedfolderPathcreates 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 passundefined/''as-is and let the generation side handle the undefined case consistently: