ljharb / qs

A querystring parser with nesting support
BSD 3-Clause "New" or "Revised" License
8.47k stars 731 forks source link

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

Open joonseokhu opened 1 year ago

joonseokhu commented 1 year ago

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;
  }
});
ljharb commented 1 year ago

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.

dbnar2 commented 1 year ago

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?

joonseokhu commented 1 year ago

@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
jtrbalic commented 2 months ago

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.

Are there any plans for this to be fixed soon? I am willing to contribute.

ljharb commented 2 months ago

@carpics if there's a way to provide this capability without a breaking change, i'd love to hear about it.