ljharb / qs

A querystring parser with nesting support

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Serialization for `Date` is not working when using `filter` option.

joonseokhu opened this issue · comments

serialization for Date type values are skipped when I'm usinng filter option for custom serialization.

code to reproduce

import qs from 'qs'

class Range {
  constructor(
    public from: number,
    public to: number
  ) {}
}

const data = {
  a: new Range(1, 123),
  b: new Date(),
  c: [new Date(), new Range(3, 45)],
  d: false,
  e: { a: 123, b: new Date(), c: new Range(6, 700) },
}

const stringified = qs.stringify(data, {
  filter(_, value) {
    if (value instanceof Range) {
      return `${value.from}...${value.to}`;
    }
    return value;
  }
});

console.log(qs.parse(stringified));

what I expected

{
  a: '1...123',
  b: '2023-03-07T09:02:41.260Z',
  c: [ '2023-03-07T09:02:41.260Z', '3...45' ],
  d: 'false',
  e: {
    a: '123',
    b: '2023-03-07T09:02:41.260Z',
    c: '6...700'
  }
}

what actually worked

{
  a: '1...123',
  c: [ '3...45' ],
  d: 'false',
  e: { a: '123', c: '6...700' }
}

so I have to call qs.stringify like this:

const stringified = qs.stringify(data, {
  filter(_, value) {
    if (value instanceof Range) {
      return `${value.from}...${value.to}`;
    }

    // I have to serialize `Date` by my self
    if (value instanceof Date) {
      return value.toISOString()
    }

    return value;
  }
});

Yes, that's true - using the filter option overrides the default filtering, which includes date serialization, as well as special handling for arrayFormat: 'comma'.

One possibility would be to pass a third argument to filter of defaultFilter, so you could return defaultFilter(_, value); and you'd get that default serialization - but we couldn't do that by default, or it'd be a breaking change.

Somewhat related to this.

Is there a way I can pass a custom prefix?

For example I have an object:
{ Age: [10,20] }

And want to serialize it so that it turns into:
age[gte]=10&age[lte]=20

The current filter only lets me return the value but not a prefix. Am i missing something here?

@dbnar2 I think there's few options that you can choose.

The point is that you should translate the array into an object, and you can get the querystring with key inside of brakets

qs.stringify({ foo: [1, 2] }, { encodeValuesOnly: true });
// foo[0]=1&foo[1]=2

qs.stringify({ foo: { bar: 1, baz: 2 } }, { encodeValuesOnly: true });
// foo[bar]=1&foo[baz]=2
  1. translate the array into object when key of the property is what you target.
const data = {
  myRange: [10, 20],
}

qs.stringify(data, {
  encodeValuesOnly: true,
  filter(key, value) {
    if (key === 'myRange') {
      const [gte, lte] = value
      return { gte, lte }
    }

    // You have to serialize `Date` by yourself
    if (value instanceof Date) {
      return value.toISOString()
    }

    return value;
  }
});

// myRange[gte]=10&myRange[lte]=20
  1. translate any array with two number elements
/**
 * returns true only if value is an array with 2 number elements.
 */
const isRange = (value: any): value is [number, number] => {
  if (!Array.isArray(value)) return false;
  if (value.length !== 2) return false;
  if (value.some(el => typeof el !== 'number')) return false;

  return true;
}

const data = {
  myRange: [10, 20],
}

qs.stringify(data, {
  encodeValuesOnly: true,
  filter(key, value) {
    if (isRange(value)) {
      const [gte, lte] = value
      return { gte, lte }
    }

    // You have to serialize `Date` by yourself
    if (value instanceof Date) {
      return value.toISOString()
    }

    return value;
  }
});
// myRange[gte]=10&myRange[lte]=20
  1. Just make and use some helperFunction before calling qs
const toNumberRange = ([gte, lte]: [number, number]) => {
  return { gte, lte }
}

const data = {
  myRange: toNumberRange([10, 20]),
}

qs.stringify(data, {
  encodeValuesOnly: true,
});
// myRange[gte]=10&myRange[lte]=20