From: Eduardo San Martin Morote Date: Wed, 27 Aug 2025 12:15:18 +0000 (+0200) Subject: feat: handle default and missing values in queries X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=a94fea68011082cd30677d4899fb831540c2fe81;p=thirdparty%2Fvuejs%2Frouter.git feat: handle default and missing values in queries --- diff --git a/packages/router/src/experimental/route-resolver/matchers/matcher-pattern-query.spec.ts b/packages/router/src/experimental/route-resolver/matchers/matcher-pattern-query.spec.ts index a0a650c3..95f7ba0c 100644 --- a/packages/router/src/experimental/route-resolver/matchers/matcher-pattern-query.spec.ts +++ b/packages/router/src/experimental/route-resolver/matchers/matcher-pattern-query.spec.ts @@ -182,6 +182,30 @@ describe('MatcherPatternQueryParam', () => { ) expect(matcher.match({ p: '5' })).toEqual({ page: '5' }) }) + + it('lets the parser handle null values', () => { + expect( + new MatcherPatternQueryParam( + 'active', + 'a', + 'value', + // transforms null to true + PARAM_PARSER_BOOL, + false + ).match({ a: null }) + ).toEqual({ active: true }) + + expect( + new MatcherPatternQueryParam( + 'active', + 'a', + 'value', + // this leavs the value as null + PARAM_PARSER_DEFAULTS, + 'ko' + ).match({ a: null }) + ).toEqual({ active: null }) + }) }) describe('parser integration', () => { @@ -301,7 +325,20 @@ describe('MatcherPatternQueryParam', () => { expect(matcher.match({ item: [] })).toEqual({ items: [] }) }) - it('filters out null values in arrays', () => { + it('integer parser filters out null values in arrays', () => { + const matcher = new MatcherPatternQueryParam( + 'ids', + 'id', + 'array', + PARAM_PARSER_INT + ) + // Integer parser filters out null values from arrays + expect(matcher.match({ id: ['1', null, '3'] })).toEqual({ + ids: [1, 3], + }) + }) + + it('integer parser with default also filters null values', () => { const matcher = new MatcherPatternQueryParam( 'ids', 'id', @@ -309,7 +346,36 @@ describe('MatcherPatternQueryParam', () => { PARAM_PARSER_INT, () => [] ) - expect(matcher.match({ id: ['1', null, '3'] })).toEqual({ ids: [1, 3] }) + // Integer parser filters null values even with default + expect(matcher.match({ id: ['1', null, '3'] })).toEqual({ + ids: [1, 3], + }) + }) + + it('passes null values to boolean parser in arrays', () => { + const matcher = new MatcherPatternQueryParam( + 'flags', + 'flag', + 'array', + PARAM_PARSER_BOOL + ) + // Now that null filtering is removed, null values get passed to parser + expect(matcher.match({ flag: ['true', null, 'false'] })).toEqual({ + flags: [true, true, false], + }) + }) + + it('handles null values with default parser in arrays', () => { + const matcher = new MatcherPatternQueryParam( + 'values', + 'value', + 'array', + PARAM_PARSER_DEFAULTS + ) + // Now that null filtering is removed, null values get passed to parser + expect(matcher.match({ value: ['a', null, 'b'] })).toEqual({ + values: ['a', null, 'b'], + }) }) it('handles undefined query param with default', () => { diff --git a/packages/router/src/experimental/route-resolver/matchers/matcher-pattern-query.ts b/packages/router/src/experimental/route-resolver/matchers/matcher-pattern-query.ts index 327be6a9..0502f027 100644 --- a/packages/router/src/experimental/route-resolver/matchers/matcher-pattern-query.ts +++ b/packages/router/src/experimental/route-resolver/matchers/matcher-pattern-query.ts @@ -4,6 +4,7 @@ import { MatcherParamsFormatted, MatcherPattern, MatcherQueryParams, + MatcherQueryParamsValue, } from './matcher-pattern' import { ParamParser, PARAM_PARSER_DEFAULTS } from './param-parsers' import { miss } from './errors' @@ -29,48 +30,41 @@ export class MatcherPatternQueryParam ) {} match(query: MatcherQueryParams): Record { - const queryValue = query[this.queryKey] + const queryValue: MatcherQueryParamsValue | undefined = query[this.queryKey] + + // Check if query param is missing for default value handling let valueBeforeParse = this.format === 'value' ? Array.isArray(queryValue) ? queryValue[0] : queryValue - : this.format === 'array' - ? Array.isArray(queryValue) - ? queryValue + : // format === 'array' + Array.isArray(queryValue) + ? queryValue + : queryValue == null + ? [] : [queryValue] - : queryValue let value: T | undefined - // if we have an array, we need to try catch each value + // if we have an array, pass the whole array to the parser if (Array.isArray(valueBeforeParse)) { - // @ts-expect-error: T is not connected to valueBeforeParse - value = [] - for (const v of valueBeforeParse) { - if (v != null) { - try { - ;(value as unknown[]).push( - // for ts errors - (this.parser.get ?? PARAM_PARSER_DEFAULTS.get)(v) as T - ) - } catch (error) { - // we skip the invalid value unless there is no defaultValue - if (this.defaultValue === undefined) { - throw error - } + // for arrays, if original query param was missing and we have a default, use it + if (queryValue === undefined && this.defaultValue !== undefined) { + value = toValue(this.defaultValue) + } else { + try { + value = (this.parser.get ?? PARAM_PARSER_DEFAULTS.get)( + valueBeforeParse + ) as T + } catch (error) { + if (this.defaultValue === undefined) { + throw error } + value = undefined } } - - // if we have no values, we want to fall back to the default value - if ( - this.defaultValue !== undefined && - (value as unknown[]).length === 0 - ) { - value = undefined - } } else { try { value = diff --git a/packages/router/src/experimental/route-resolver/matchers/matcher-pattern.spec.ts b/packages/router/src/experimental/route-resolver/matchers/matcher-pattern.spec.ts index 79e796f5..fc52ad67 100644 --- a/packages/router/src/experimental/route-resolver/matchers/matcher-pattern.spec.ts +++ b/packages/router/src/experimental/route-resolver/matchers/matcher-pattern.spec.ts @@ -430,6 +430,16 @@ describe('MatcherPatternPathDynamic', () => { set: (v: number | null) => (v == null ? null : String(v / 2)), }) + const nullAwareParser = definePathParamParser({ + get: (v: string | null) => { + if (v === null) return 'was-null' + if (v === undefined) return 'was-undefined' + return `processed-${v}` + }, + set: (v: string | null) => + v === 'was-null' ? null : String(v).replace('processed-', ''), + }) + it('single regular param', () => { const pattern = new MatcherPatternPathDynamic( /^\/teams\/([^/]+?)$/i, @@ -460,5 +470,22 @@ describe('MatcherPatternPathDynamic', () => { expect(pattern.build({ teamId: 0 })).toBe('/teams/0') expect(pattern.build({ teamId: null })).toBe('/teams') }) + + it('handles null values in optional params with custom parser', () => { + const pattern = new MatcherPatternPathDynamic( + /^\/teams(?:\/([^/]+?))?$/i, + { + teamId: [nullAwareParser, false, true], + }, + ['teams', 1] + ) + + expect(pattern.match('/teams')).toEqual({ teamId: 'was-null' }) + expect(pattern.match('/teams/hello')).toEqual({ + teamId: 'processed-hello', + }) + expect(pattern.build({ teamId: 'was-null' })).toBe('/teams') + expect(pattern.build({ teamId: 'processed-world' })).toBe('/teams/world') + }) }) }) diff --git a/packages/router/src/experimental/route-resolver/matchers/param-parsers/booleans.spec.ts b/packages/router/src/experimental/route-resolver/matchers/param-parsers/booleans.spec.ts index 3d24152a..b32f44c7 100644 --- a/packages/router/src/experimental/route-resolver/matchers/param-parsers/booleans.spec.ts +++ b/packages/router/src/experimental/route-resolver/matchers/param-parsers/booleans.spec.ts @@ -66,6 +66,25 @@ describe('PARAM_PARSER_BOOL', () => { expect(PARAM_PARSER_BOOL.get([null, null])).toEqual([true, true]) }) + it('handles mixed arrays with null values correctly', () => { + expect(PARAM_PARSER_BOOL.get([null, 'true', null])).toEqual([ + true, + true, + true, + ]) + expect(PARAM_PARSER_BOOL.get(['false', null, 'TRUE'])).toEqual([ + false, + true, + true, + ]) + expect(PARAM_PARSER_BOOL.get([null, 'false', null, 'true'])).toEqual([ + true, + false, + true, + true, + ]) + }) + it('throws for arrays with invalid values', () => { expect(() => PARAM_PARSER_BOOL.get(['true', 'invalid'])).toThrow() expect(() => PARAM_PARSER_BOOL.get(['invalid'])).toThrow() diff --git a/packages/router/src/experimental/route-resolver/matchers/param-parsers/integers.spec.ts b/packages/router/src/experimental/route-resolver/matchers/param-parsers/integers.spec.ts index 2c7fb5e1..68435586 100644 --- a/packages/router/src/experimental/route-resolver/matchers/param-parsers/integers.spec.ts +++ b/packages/router/src/experimental/route-resolver/matchers/param-parsers/integers.spec.ts @@ -92,9 +92,10 @@ describe('PARAM_PARSER_INT', () => { expect(() => PARAM_PARSER_INT.get(['', '2'])).toThrow() }) - it('throws for arrays with null values', () => { - expect(() => PARAM_PARSER_INT.get(['1', null, '3'])).toThrow() - expect(() => PARAM_PARSER_INT.get([null])).toThrow() + it('filters out null values from arrays', () => { + expect(PARAM_PARSER_INT.get(['1', null, '3'])).toEqual([1, 3]) + expect(PARAM_PARSER_INT.get([null])).toEqual([]) + expect(PARAM_PARSER_INT.get(['42', null, null, '7'])).toEqual([42, 7]) }) }) diff --git a/packages/router/src/experimental/route-resolver/matchers/param-parsers/integers.ts b/packages/router/src/experimental/route-resolver/matchers/param-parsers/integers.ts index 79720483..31b007c4 100644 --- a/packages/router/src/experimental/route-resolver/matchers/param-parsers/integers.ts +++ b/packages/router/src/experimental/route-resolver/matchers/param-parsers/integers.ts @@ -13,7 +13,8 @@ const PARAM_INTEGER_SINGLE = { } satisfies ParamParser const PARAM_INTEGER_REPEATABLE = { - get: (value: (string | null)[]) => value.map(PARAM_INTEGER_SINGLE.get), + get: (value: (string | null)[]) => + value.filter((v): v is string => v != null).map(PARAM_INTEGER_SINGLE.get), set: (value: number[]) => value.map(PARAM_INTEGER_SINGLE.set), } satisfies ParamParser