diff --git a/npm/ng-packs/packages/schematics/src/tests/get-param-value-name.spec.ts b/npm/ng-packs/packages/schematics/src/tests/get-param-value-name.spec.ts new file mode 100644 index 0000000000..6defc9ddfa --- /dev/null +++ b/npm/ng-packs/packages/schematics/src/tests/get-param-value-name.spec.ts @@ -0,0 +1,26 @@ +import { describe, expect, it } from 'vitest'; +import { getParamValueName } from '../utils/methods'; + +describe('getParamValueName', () => { + it('should navigate nested params with optional chaining', () => { + expect(getParamValueName('NestedFilter.SomeField', 'input')).toBe( + 'input?.nestedFilter?.someField', + ); + }); + + it('should optional-chain every level of a deeper nested param', () => { + expect(getParamValueName('Filter.Inner.Value', 'input')).toBe('input?.filter?.inner?.value'); + }); + + it('should not add optional chaining for a non-nested param', () => { + expect(getParamValueName('someField', 'input')).toBe('input.someField'); + }); + + it('should keep bracket access for a non-nested param that needs quoting', () => { + expect(getParamValueName('some-field', 'input')).toBe("input['some-field']"); + }); + + it('should quote a nested segment that needs quoting and still optional-chain', () => { + expect(getParamValueName('Filter.some-field', 'input')).toBe("input?.filter?.['some-field']"); + }); +}); diff --git a/npm/ng-packs/packages/schematics/src/tests/proxy-nested-optional-chaining.spec.ts b/npm/ng-packs/packages/schematics/src/tests/proxy-nested-optional-chaining.spec.ts new file mode 100644 index 0000000000..5c4bee4540 --- /dev/null +++ b/npm/ng-packs/packages/schematics/src/tests/proxy-nested-optional-chaining.spec.ts @@ -0,0 +1,40 @@ +import { describe, expect, it } from 'vitest'; +import { createActionToBodyMapper } from '../utils/service'; +import { eBindingSourceId } from '../enums'; +import { Action } from '../models/api-definition'; + +// Exercises the real proxy-generation pipeline (registerActionParameter -> getParamValueName -> +// Body.params) for a flattened nullable nested query param, the case reported in #25176. +describe('proxy generation - nested nullable query param', () => { + const action = { + uniqueName: 'GetListAsync', + name: 'GetListAsync', + httpMethod: 'GET', + url: 'api/app/my', + supportedVersions: [], + parametersOnMethod: [], + parameters: [ + { + nameOnMethod: 'input', + name: 'NestedFilter.SomeField', + jsonName: null, + type: 'System.String', + typeSimple: 'string', + isOptional: true, + defaultValue: null, + constraintTypes: null, + bindingSourceId: eBindingSourceId.Query, + descriptorName: 'input', + }, + ], + returnValue: { type: 'System.Void', typeSimple: 'void' }, + } as unknown as Action; + + it('emits optional chaining in the generated params object', () => { + const body = createActionToBodyMapper()(action); + + expect(body.params).toContain( + '["NestedFilter.SomeField"]: input?.nestedFilter?.someField', + ); + }); +}); diff --git a/npm/ng-packs/packages/schematics/src/utils/methods.ts b/npm/ng-packs/packages/schematics/src/utils/methods.ts index 6d31903a78..246c86541f 100644 --- a/npm/ng-packs/packages/schematics/src/utils/methods.ts +++ b/npm/ng-packs/packages/schematics/src/utils/methods.ts @@ -7,7 +7,9 @@ export const getParamName = (paramName: string) => export const getParamValueName = (paramName: string, descriptorName: string) => { if (paramName.includes('.')) { const splitted = paramName.split('.'); - const param = splitted.map(x => (shouldQuote(x) ? `[${x}]` : `.${camel(x)}`)).join(''); + // Flattened nested query params: any intermediate object may be absent at runtime, and + // nested members are typed as `T | null`, so navigate them with optional chaining (`?.`). + const param = splitted.map(x => (shouldQuote(x) ? `?.['${x}']` : `?.${camel(x)}`)).join(''); return `${descriptorName}${param}`; } if (shouldQuote(paramName)) {