Support template variables in command menu item labels (#18707)

- Adds template variable interpolation (`${...}`) to command menu item
labels and short labels, enabling dynamic text like `Create new
${capitalize(objectMetadataItem.labelSingular)}` instead of static
`Create new record`.
- Supports `capitalize` and `lowercase` transform functions within
template expressions.
This commit is contained in:
Raphaël Bosi
2026-03-18 11:26:58 +01:00
committed by GitHub
parent 928194cee4
commit dedcf4e9b9
25 changed files with 673 additions and 236 deletions
@@ -0,0 +1,385 @@
import {
CommandMenuContextApiPageType,
type CommandMenuContextApi,
} from '@/types';
import { evaluateConditionalAvailabilityExpression } from '../evaluateConditionalAvailabilityExpression';
const buildContext = (
overrides: Partial<CommandMenuContextApi> = {},
): CommandMenuContextApi => ({
pageType: CommandMenuContextApiPageType.INDEX_PAGE,
isInSidePanel: false,
isPageInEditMode: false,
favoriteRecordIds: [],
isSelectAll: false,
hasAnySoftDeleteFilterOnView: false,
numberOfSelectedRecords: 0,
objectPermissions: {
objectMetadataId: '',
canReadObjectRecords: true,
canUpdateObjectRecords: true,
canSoftDeleteObjectRecords: true,
canDestroyObjectRecords: false,
restrictedFields: {},
rowLevelPermissionPredicates: [],
rowLevelPermissionPredicateGroups: [],
},
selectedRecords: [],
featureFlags: {},
targetObjectReadPermissions: {},
targetObjectWritePermissions: {},
objectMetadataItem: {},
...overrides,
});
describe('evaluateConditionalAvailabilityExpression', () => {
describe('arrayLength for favoriteRecordIds', () => {
it('should evaluate arrayLength(favoriteRecordIds) < numberOfSelectedRecords when some selected are not favorites', () => {
const expression =
'arrayLength(favoriteRecordIds) < numberOfSelectedRecords and noneDefined(selectedRecords, "deletedAt") and not hasAnySoftDeleteFilterOnView';
const context = buildContext({
favoriteRecordIds: ['id-1'],
numberOfSelectedRecords: 2,
selectedRecords: [
{ id: 'id-1', createdAt: '', updatedAt: '', deletedAt: null },
{ id: 'id-2', createdAt: '', updatedAt: '', deletedAt: null },
],
hasAnySoftDeleteFilterOnView: false,
});
expect(
evaluateConditionalAvailabilityExpression(expression, context),
).toBe(true);
});
it('should deny when all selected are already favorites', () => {
const expression =
'arrayLength(favoriteRecordIds) < numberOfSelectedRecords and noneDefined(selectedRecords, "deletedAt") and not hasAnySoftDeleteFilterOnView';
const context = buildContext({
favoriteRecordIds: ['id-1', 'id-2'],
numberOfSelectedRecords: 2,
selectedRecords: [
{ id: 'id-1', createdAt: '', updatedAt: '', deletedAt: null },
{ id: 'id-2', createdAt: '', updatedAt: '', deletedAt: null },
],
hasAnySoftDeleteFilterOnView: false,
});
expect(
evaluateConditionalAvailabilityExpression(expression, context),
).toBe(false);
});
it('should evaluate arrayLength(favoriteRecordIds) == numberOfSelectedRecords when all selected are favorites', () => {
const expression =
'arrayLength(favoriteRecordIds) == numberOfSelectedRecords and noneDefined(selectedRecords, "deletedAt") and not hasAnySoftDeleteFilterOnView';
const context = buildContext({
favoriteRecordIds: ['id-1', 'id-2'],
numberOfSelectedRecords: 2,
selectedRecords: [
{ id: 'id-1', createdAt: '', updatedAt: '', deletedAt: null },
{ id: 'id-2', createdAt: '', updatedAt: '', deletedAt: null },
],
hasAnySoftDeleteFilterOnView: false,
});
expect(
evaluateConditionalAvailabilityExpression(expression, context),
).toBe(true);
});
it('should reject favoriteRecordIds.length (expr-eval-fork parses "length" as unary op)', () => {
const expression = 'favoriteRecordIds.length < numberOfSelectedRecords';
const context = buildContext({
favoriteRecordIds: ['id-1'],
numberOfSelectedRecords: 2,
});
expect(
evaluateConditionalAvailabilityExpression(expression, context),
).toBe(false);
});
});
describe('includesSome', () => {
it('should return true when some records include the value in the array prop', () => {
const expression = 'includesSome(selectedRecords, "statuses", "ACTIVE")';
const context = buildContext({
selectedRecords: [
{ id: 'id-1', statuses: ['ACTIVE'] },
{ id: 'id-2', statuses: ['DRAFT'] },
],
});
expect(
evaluateConditionalAvailabilityExpression(expression, context),
).toBe(true);
});
it('should return false when no record includes the value in the array prop', () => {
const expression = 'includesSome(selectedRecords, "statuses", "ACTIVE")';
const context = buildContext({
selectedRecords: [
{ id: 'id-1', statuses: ['DRAFT'] },
{ id: 'id-2', statuses: ['DRAFT'] },
],
});
expect(
evaluateConditionalAvailabilityExpression(expression, context),
).toBe(false);
});
});
describe('includesNone', () => {
it('should return true when no record includes the value in the array prop', () => {
const expression = 'includesNone(selectedRecords, "statuses", "ACTIVE")';
const context = buildContext({
selectedRecords: [
{ id: 'id-1', statuses: ['DRAFT'] },
{ id: 'id-2', statuses: ['DRAFT'] },
],
});
expect(
evaluateConditionalAvailabilityExpression(expression, context),
).toBe(true);
});
it('should return false when any record includes the value in the array prop', () => {
const expression = 'includesNone(selectedRecords, "statuses", "ACTIVE")';
const context = buildContext({
selectedRecords: [
{ id: 'id-1', statuses: ['ACTIVE'] },
{ id: 'id-2', statuses: ['DRAFT'] },
],
});
expect(
evaluateConditionalAvailabilityExpression(expression, context),
).toBe(false);
});
it('should return false when all records include the value in the array prop', () => {
const expression = 'includesNone(selectedRecords, "statuses", "ACTIVE")';
const context = buildContext({
selectedRecords: [
{ id: 'id-1', statuses: ['ACTIVE', 'DRAFT'] },
{ id: 'id-2', statuses: ['ACTIVE'] },
],
});
expect(
evaluateConditionalAvailabilityExpression(expression, context),
).toBe(false);
});
});
describe('empty array guard against vacuous truth', () => {
it('should return false for every() on empty selectedRecords', () => {
const context = buildContext({ selectedRecords: [] });
expect(
evaluateConditionalAvailabilityExpression(
'every(selectedRecords, "active")',
context,
),
).toBe(false);
});
it('should return false for everyDefined() on empty selectedRecords', () => {
const context = buildContext({ selectedRecords: [] });
expect(
evaluateConditionalAvailabilityExpression(
'everyDefined(selectedRecords, "name")',
context,
),
).toBe(false);
});
it('should return false for none() on empty selectedRecords', () => {
const context = buildContext({ selectedRecords: [] });
expect(
evaluateConditionalAvailabilityExpression(
'none(selectedRecords, "deletedAt")',
context,
),
).toBe(false);
});
it('should return false for noneDefined() on empty selectedRecords', () => {
const context = buildContext({ selectedRecords: [] });
expect(
evaluateConditionalAvailabilityExpression(
'noneDefined(selectedRecords, "deletedAt")',
context,
),
).toBe(false);
});
it('should return false for everyEquals() on empty selectedRecords', () => {
const context = buildContext({ selectedRecords: [] });
expect(
evaluateConditionalAvailabilityExpression(
'everyEquals(selectedRecords, "status", "DRAFT")',
context,
),
).toBe(false);
});
it('should return false for noneEquals() on empty selectedRecords', () => {
const context = buildContext({ selectedRecords: [] });
expect(
evaluateConditionalAvailabilityExpression(
'noneEquals(selectedRecords, "status", "ACTIVE")',
context,
),
).toBe(false);
});
it('should return false for includesEvery() on empty selectedRecords', () => {
const context = buildContext({ selectedRecords: [] });
expect(
evaluateConditionalAvailabilityExpression(
'includesEvery(selectedRecords, "statuses", "ACTIVE")',
context,
),
).toBe(false);
});
it('should return false for includesNone() on empty selectedRecords', () => {
const context = buildContext({ selectedRecords: [] });
expect(
evaluateConditionalAvailabilityExpression(
'includesNone(selectedRecords, "statuses", "ACTIVE")',
context,
),
).toBe(false);
});
it('should return false for some() on empty selectedRecords', () => {
const context = buildContext({ selectedRecords: [] });
expect(
evaluateConditionalAvailabilityExpression(
'some(selectedRecords, "active")',
context,
),
).toBe(false);
});
it('should return false for someDefined() on empty selectedRecords', () => {
const context = buildContext({ selectedRecords: [] });
expect(
evaluateConditionalAvailabilityExpression(
'someDefined(selectedRecords, "name")',
context,
),
).toBe(false);
});
it('should return false for includesSome() on empty selectedRecords', () => {
const context = buildContext({ selectedRecords: [] });
expect(
evaluateConditionalAvailabilityExpression(
'includesSome(selectedRecords, "statuses", "ACTIVE")',
context,
),
).toBe(false);
});
});
describe('activateWorkflow expression regression', () => {
const activateWorkflowExpression =
'everyDefined(selectedRecords, "currentVersion.trigger") and everyDefined(selectedRecords, "currentVersion.steps") and every(selectedRecords, "currentVersion.steps.length") and (everyEquals(selectedRecords, "currentVersion.status", "DRAFT") or includesNone(selectedRecords, "statuses", "ACTIVE")) and noneDefined(selectedRecords, "deletedAt")';
it('should not show Activate for an already-active workflow', () => {
const context = buildContext({
selectedRecords: [
{
id: 'wf-1',
deletedAt: null,
statuses: ['ACTIVE'],
currentVersion: {
status: 'ACTIVE',
trigger: { type: 'MANUAL' },
steps: [{ id: 'step-1' }],
},
},
],
});
expect(
evaluateConditionalAvailabilityExpression(
activateWorkflowExpression,
context,
),
).toBe(false);
});
it('should show Activate for a draft workflow with no active version', () => {
const context = buildContext({
selectedRecords: [
{
id: 'wf-1',
deletedAt: null,
statuses: ['DRAFT'],
currentVersion: {
status: 'DRAFT',
trigger: { type: 'MANUAL' },
steps: [{ id: 'step-1' }],
},
},
],
});
expect(
evaluateConditionalAvailabilityExpression(
activateWorkflowExpression,
context,
),
).toBe(true);
});
it('should show Activate for a draft version when another version is active', () => {
const context = buildContext({
selectedRecords: [
{
id: 'wf-1',
deletedAt: null,
statuses: ['ACTIVE', 'DRAFT'],
currentVersion: {
status: 'DRAFT',
trigger: { type: 'MANUAL' },
steps: [{ id: 'step-1' }],
},
},
],
});
expect(
evaluateConditionalAvailabilityExpression(
activateWorkflowExpression,
context,
),
).toBe(true);
});
});
});
@@ -0,0 +1,316 @@
import {
CommandMenuContextApiPageType,
type CommandMenuContextApi,
} from '@/types';
import { interpolateCommandMenuItemLabel } from '../interpolateCommandMenuItemLabel';
const buildContext = (
overrides: Partial<CommandMenuContextApi> = {},
): CommandMenuContextApi => ({
pageType: CommandMenuContextApiPageType.INDEX_PAGE,
isInSidePanel: false,
isPageInEditMode: false,
favoriteRecordIds: [],
isSelectAll: false,
hasAnySoftDeleteFilterOnView: false,
numberOfSelectedRecords: 0,
objectPermissions: {
objectMetadataId: '',
canReadObjectRecords: true,
canUpdateObjectRecords: true,
canSoftDeleteObjectRecords: true,
canDestroyObjectRecords: false,
restrictedFields: {},
rowLevelPermissionPredicates: [],
rowLevelPermissionPredicateGroups: [],
},
selectedRecords: [],
featureFlags: {},
targetObjectReadPermissions: {},
targetObjectWritePermissions: {},
objectMetadataItem: {},
...overrides,
});
describe('interpolateCommandMenuItemLabel', () => {
describe('sequential invocations (global regex lastIndex)', () => {
it('should interpolate correctly when called multiple times in sequence', () => {
const context = buildContext({
numberOfSelectedRecords: 2,
objectMetadataItem: { labelPlural: 'people' },
});
expect(
interpolateCommandMenuItemLabel({
label: 'First: ${numberOfSelectedRecords}',
context,
}),
).toBe('First: 2');
expect(
interpolateCommandMenuItemLabel({
label: 'Second: ${objectMetadataItem.labelPlural}',
context,
}),
).toBe('Second: people');
});
});
describe('plain strings without template variables', () => {
it('should return the label unchanged when no template variables are present', () => {
const context = buildContext();
expect(
interpolateCommandMenuItemLabel({ label: 'Delete', context }),
).toBe('Delete');
});
it('should return null when label is null', () => {
const context = buildContext();
expect(
interpolateCommandMenuItemLabel({ label: null, context }),
).toBeNull();
});
it('should return null when label is undefined', () => {
const context = buildContext();
expect(
interpolateCommandMenuItemLabel({ label: undefined, context }),
).toBeNull();
});
it('should return an empty string for an empty label', () => {
const context = buildContext();
expect(interpolateCommandMenuItemLabel({ label: '', context })).toBe('');
});
});
describe('simple template variable interpolation', () => {
it('should interpolate a top-level context property', () => {
const context = buildContext({ numberOfSelectedRecords: 5 });
expect(
interpolateCommandMenuItemLabel({
label: 'Selected: ${numberOfSelectedRecords}',
context,
}),
).toBe('Selected: 5');
});
it('should interpolate objectMetadataItem.labelSingular', () => {
const context = buildContext({
objectMetadataItem: { labelSingular: 'person' },
});
expect(
interpolateCommandMenuItemLabel({
label: 'Create new ${objectMetadataItem.labelSingular}',
context,
}),
).toBe('Create new person');
});
it('should interpolate objectMetadataItem.labelPlural', () => {
const context = buildContext({
objectMetadataItem: { labelPlural: 'companies' },
});
expect(
interpolateCommandMenuItemLabel({
label: 'Delete ${objectMetadataItem.labelPlural}',
context,
}),
).toBe('Delete companies');
});
});
describe('multiple template variables', () => {
it('should interpolate multiple variables in one label', () => {
const context = buildContext({
numberOfSelectedRecords: 3,
objectMetadataItem: { labelPlural: 'people' },
});
expect(
interpolateCommandMenuItemLabel({
label:
'${numberOfSelectedRecords} ${objectMetadataItem.labelPlural} selected',
context,
}),
).toBe('3 people selected');
});
});
describe('nested property access', () => {
it('should resolve deeply nested properties', () => {
const context = buildContext({
objectMetadataItem: {
labelSingular: 'opportunity',
nameSingular: 'opportunity',
},
});
expect(
interpolateCommandMenuItemLabel({
label: 'New ${objectMetadataItem.nameSingular}',
context,
}),
).toBe('New opportunity');
});
});
describe('missing context values', () => {
it('should return empty string for undefined nested property', () => {
const context = buildContext({
objectMetadataItem: {},
});
expect(
interpolateCommandMenuItemLabel({
label: 'New ${objectMetadataItem.labelSingular}',
context,
}),
).toBe('New ');
});
it('should return empty string for empty objectMetadataItem', () => {
const context = buildContext();
expect(
interpolateCommandMenuItemLabel({
label: 'Create new ${objectMetadataItem.labelSingular}',
context,
}),
).toBe('Create new ');
});
});
describe('unresolvable property paths', () => {
it('should return empty string for a path that cannot be resolved', () => {
const context = buildContext();
expect(
interpolateCommandMenuItemLabel({
label: 'Label ${nonExistent.deep.path}',
context,
}),
).toBe('Label ');
});
it('should not resolve inherited Object.prototype members', () => {
const context = buildContext();
expect(
interpolateCommandMenuItemLabel({
label: 'Val: ${toString}',
context,
}),
).toBe('Val: ');
expect(
interpolateCommandMenuItemLabel({
label: 'Val: ${objectMetadataItem.hasOwnProperty}',
context,
}),
).toBe('Val: ');
});
});
describe('boolean context values', () => {
it('should stringify boolean values', () => {
const context = buildContext({ isInSidePanel: true });
expect(
interpolateCommandMenuItemLabel({
label: 'Side panel: ${isInSidePanel}',
context,
}),
).toBe('Side panel: true');
});
it('should pass through string values as-is without forced lowercasing', () => {
const context = buildContext({
objectMetadataItem: { labelSingular: 'Person' },
});
expect(
interpolateCommandMenuItemLabel({
label: 'Create new ${objectMetadataItem.labelSingular}',
context,
}),
).toBe('Create new Person');
});
});
describe('capitalize transform', () => {
it('should capitalize the first character of a resolved value', () => {
const context = buildContext({
objectMetadataItem: { labelSingular: 'person' },
});
expect(
interpolateCommandMenuItemLabel({
label: '${capitalize(objectMetadataItem.labelSingular)} details',
context,
}),
).toBe('Person details');
});
it('should capitalize a mixed-case value', () => {
const context = buildContext({
objectMetadataItem: { labelPlural: 'companyRecords' },
});
expect(
interpolateCommandMenuItemLabel({
label: '${capitalize(objectMetadataItem.labelPlural)} selected',
context,
}),
).toBe('CompanyRecords selected');
});
it('should return empty string when the nested property is undefined', () => {
const context = buildContext({
objectMetadataItem: {},
});
expect(
interpolateCommandMenuItemLabel({
label: '${capitalize(objectMetadataItem.labelSingular)} details',
context,
}),
).toBe(' details');
});
});
describe('lowercase transform', () => {
it('should lowercase the resolved value', () => {
const context = buildContext({
objectMetadataItem: { labelSingular: 'Person' },
});
expect(
interpolateCommandMenuItemLabel({
label: 'Create ${lowercase(objectMetadataItem.labelSingular)}',
context,
}),
).toBe('Create person');
});
it('should lowercase all characters', () => {
const context = buildContext({
objectMetadataItem: { labelPlural: 'People' },
});
expect(
interpolateCommandMenuItemLabel({
label: 'Delete ${lowercase(objectMetadataItem.labelPlural)}',
context,
}),
).toBe('Delete people');
});
});
});
@@ -0,0 +1,120 @@
import { isNonEmptyArray, isNonEmptyString } from '@sniptt/guards';
import { Parser } from 'expr-eval-fork';
import { isDefined } from '../validation/isDefined';
import { safeGetNestedProperty } from './safeGetNestedProperty';
type ArrayMethod = 'every' | 'some';
const createArrayPropCheck = (
method: ArrayMethod,
predicate: (value: unknown) => boolean,
) => {
return (array: unknown, prop: string) => {
if (!isNonEmptyArray(array)) {
return false;
}
return array[method]((item) =>
predicate(safeGetNestedProperty(item, prop)),
);
};
};
const createArrayPropValueCheck = (
method: ArrayMethod,
predicate: (value: unknown, target: unknown) => boolean,
) => {
return (array: unknown, prop: string, value: unknown) => {
if (!isNonEmptyArray(array)) {
return false;
}
return array[method]((item) =>
predicate(safeGetNestedProperty(item, prop), value),
);
};
};
export const conditionalAvailabilityParser = new Parser();
conditionalAvailabilityParser.functions.isDefined = (value: unknown) =>
isDefined(value);
conditionalAvailabilityParser.functions.isNonEmptyString = (value: unknown) =>
isNonEmptyString(value);
conditionalAvailabilityParser.functions.includes = (
array: unknown,
value: unknown,
) => Array.isArray(array) && array.includes(value);
conditionalAvailabilityParser.functions.arrayLength = (value: unknown) =>
Array.isArray(value) ? value.length : 0;
conditionalAvailabilityParser.functions.every = createArrayPropCheck(
'every',
Boolean,
);
conditionalAvailabilityParser.functions.everyDefined = createArrayPropCheck(
'every',
isDefined,
);
conditionalAvailabilityParser.functions.some = createArrayPropCheck(
'some',
Boolean,
);
conditionalAvailabilityParser.functions.someDefined = createArrayPropCheck(
'some',
isDefined,
);
conditionalAvailabilityParser.functions.someNonEmptyString =
createArrayPropCheck('some', isNonEmptyString);
conditionalAvailabilityParser.functions.none = createArrayPropCheck(
'every',
(value) => !Boolean(value),
);
conditionalAvailabilityParser.functions.noneDefined = createArrayPropCheck(
'every',
(value) => !isDefined(value),
);
conditionalAvailabilityParser.functions.everyEquals = createArrayPropValueCheck(
'every',
(a, b) => a === b,
);
conditionalAvailabilityParser.functions.someEquals = createArrayPropValueCheck(
'some',
(a, b) => a === b,
);
conditionalAvailabilityParser.functions.noneEquals = createArrayPropValueCheck(
'every',
(a, b) => a !== b,
);
conditionalAvailabilityParser.functions.includesEvery =
createArrayPropValueCheck(
'every',
(array, value) => Array.isArray(array) && array.includes(value),
);
conditionalAvailabilityParser.functions.includesSome =
createArrayPropValueCheck(
'some',
(array, value) => Array.isArray(array) && array.includes(value),
);
conditionalAvailabilityParser.functions.includesNone =
createArrayPropValueCheck(
'every',
(array, value) => Array.isArray(array) && !array.includes(value),
);
@@ -0,0 +1,21 @@
import { isNonEmptyString } from '@sniptt/guards';
import { type EvaluationContext } from 'expr-eval-fork';
import { conditionalAvailabilityParser } from './conditionalAvailabilityParser';
export const evaluateConditionalAvailabilityExpression = (
expression: string | null | undefined,
context: EvaluationContext,
): boolean => {
if (!isNonEmptyString(expression)) {
return true;
}
try {
const parsed = conditionalAvailabilityParser.parse(expression);
return parsed.evaluate(context) === true;
} catch {
return false;
}
};
@@ -0,0 +1,75 @@
import { type Nullable } from '@/types';
import { capitalize } from '../strings/capitalize';
import { isDefined } from '../validation/isDefined';
import { safeGetNestedProperty } from './safeGetNestedProperty';
const TEMPLATE_VARIABLE_REGEX = /\$\{([^{}]+)\}/g;
const HAS_TEMPLATE_VARIABLE_REGEX = /\$\{[^{}]+\}/;
const TRANSFORM_FUNCTION_CALL_REGEX = /^(\w+)\((.+)\)$/;
const LABEL_TRANSFORM_FUNCTIONS: Record<string, (value: string) => string> = {
capitalize,
lowercase: (value: string) => value.toLowerCase(),
};
const resolveTemplateExpression = ({
expression,
context,
}: {
expression: string;
context: Record<string, unknown>;
}): string => {
const trimmedExpression = expression.trim();
const transformFunctionMatch = trimmedExpression.match(
TRANSFORM_FUNCTION_CALL_REGEX,
);
const expressionToEvaluate = transformFunctionMatch
? transformFunctionMatch[2].trim()
: trimmedExpression;
const transformFunction = transformFunctionMatch
? LABEL_TRANSFORM_FUNCTIONS[transformFunctionMatch[1]]
: undefined;
const resolvedPropertyValue = safeGetNestedProperty(
context,
expressionToEvaluate,
);
if (!isDefined(resolvedPropertyValue)) {
return '';
}
const stringValue = String(resolvedPropertyValue);
return isDefined(transformFunction)
? transformFunction(stringValue)
: stringValue;
};
export const interpolateCommandMenuItemLabel = ({
label,
context,
}: {
label: Nullable<string>;
context: Record<string, unknown>;
}): Nullable<string> => {
if (!isDefined(label)) {
return null;
}
if (!HAS_TEMPLATE_VARIABLE_REGEX.test(label)) {
return label;
}
return label.replace(TEMPLATE_VARIABLE_REGEX, (match, expression: string) => {
try {
return resolveTemplateExpression({ expression, context });
} catch {
return match;
}
});
};
@@ -0,0 +1,39 @@
import { isObject, isString } from '@sniptt/guards';
import { isDefined } from '../validation/isDefined';
const BLOCKED_PROPERTY_NAMES = new Set([
'__proto__',
'constructor',
'prototype',
]);
export const safeGetNestedProperty = (
objectToEvaluate: unknown,
path: string,
): unknown => {
if (!isString(path)) {
return undefined;
}
const parts = path.split('.');
let currentObject: unknown = objectToEvaluate;
for (const part of parts) {
if (!isDefined(currentObject) || !isObject(currentObject)) {
return undefined;
}
if (
BLOCKED_PROPERTY_NAMES.has(part) ||
!Object.prototype.hasOwnProperty.call(currentObject, part)
) {
return undefined;
}
currentObject = (currentObject as Record<string, unknown>)[part];
}
return currentObject;
};