]> git.ipfire.org Git - thirdparty/vuejs/router.git/commitdiff
feat: handle default and missing values in queries
authorEduardo San Martin Morote <posva13@gmail.com>
Wed, 27 Aug 2025 12:15:18 +0000 (14:15 +0200)
committerEduardo San Martin Morote <posva13@gmail.com>
Wed, 27 Aug 2025 12:15:18 +0000 (14:15 +0200)
packages/router/src/experimental/route-resolver/matchers/matcher-pattern-query.spec.ts
packages/router/src/experimental/route-resolver/matchers/matcher-pattern-query.ts
packages/router/src/experimental/route-resolver/matchers/matcher-pattern.spec.ts
packages/router/src/experimental/route-resolver/matchers/param-parsers/booleans.spec.ts
packages/router/src/experimental/route-resolver/matchers/param-parsers/integers.spec.ts
packages/router/src/experimental/route-resolver/matchers/param-parsers/integers.ts

index a0a650c33f10c7b30842cc963ba93dcb69e9df15..95f7ba0cd091df9e84e1db348c964c3a5d8162a4 100644 (file)
@@ -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', () => {
index 327be6a97b332ee7d8a4c84a0f6d0caad9a6ffa9..0502f027837902883293ec81c503b8e355591785 100644 (file)
@@ -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<T, ParamName extends string>
   ) {}
 
   match(query: MatcherQueryParams): Record<ParamName, T> {
-    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 =
index 79e796f588f4b78b532a8a72f1cee4d960c96b60..fc52ad67db3023dcee401353cb2494fe1b7daa18 100644 (file)
@@ -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')
+    })
   })
 })
index 3d24152a538023d3ac33a8be425ab81ae238b0c8..b32f44c7faf9191fe80587281c9d153096a68eec 100644 (file)
@@ -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()
index 2c7fb5e1046b76346ee6bc4bc7fc7a0e0bf7e075..68435586452bdc52f945ae3d5056fe5c1b2a4581 100644 (file)
@@ -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])
     })
   })
 
index 79720483bd69d7f52b137c6b622b92e3ef5e564f..31b007c4b8e69f3bc2597c09c0d96fb55e6f2af2 100644 (file)
@@ -13,7 +13,8 @@ const PARAM_INTEGER_SINGLE = {
 } satisfies ParamParser<number, string | null>
 
 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<number[], (string | null)[]>