pbeshai/serialize-query-params

how to compose encoding/decoding params?

sibelius opened this issue · 2 comments

My use case is to keep all filters/search options of a screen in query string

I'd like to declare complex encoding/decoding params to make sure all filters keys are parsed properly

example of date range param

import moment, { Moment } from 'moment';
import { QueryParamConfig, encodeObject, decodeObject } from 'use-query-params';

type DateRange = {
  begin: Moment;
  end: Moment;
};

export const DateRangeParam: QueryParamConfig<DateRange> = {
  encode(dateRangeObj: { begin: Moment; end: Moment }): string | undefined {
    if (dateRangeObj === null) {
      return undefined;
    }

    if (!dateRangeObj.begin) {
      return encodeObject({
        end: dateRangeObj.end.toISOString(),
      });
    }

    if (!dateRangeObj.end) {
      return encodeObject({
        begin: dateRangeObj.begin.toISOString(),
      });
    }

    return encodeObject({
      begin: dateRangeObj.begin.toISOString(),
      end: dateRangeObj.end.toISOString(),
    });
  },

  decode(input: string | string[] | null | undefined): Moment | undefined {
    if (input == null || !input.length) {
      return undefined;
    }

    const dateRangeObj = decodeObject(input);

    return {
      begin: dateRangeObj.begin ? moment(dateRangeObj.begin) : null,
      end: dateRangeObj.end ? moment(dateRangeObj.end) : null,
    };
  },
};

example: date=begin-2020-01-01T03%3A00%3A00.000Z_end-2020-01-31T03%3A00%3A00.000Z

I'd like to easily to something like this:

ObjectParams<{begin: MomentParam, end: MomentParam>

similar to yup validation schema (https://github.com/jquense/yup)

create a simpler Param builder

export const getSchemaParam = (objParam: QueryParamConfig<any>): QueryParamConfig<any> => {
  return {
    encode(input: object) {
      const objEncoded = Object.keys(input).reduce((acc, key) => {
        const value = input[key];
        const param = objParam[key];

        // is it a QueryParam?
        if (param && param.encode && param.decode) {
          return {
            ...acc,
            [key]: param.encode(value),
          };
        }

        return {
          ...acc,
          [key]: value,
        };
      }, {});

      return encodeObject(objEncoded);
    },
    decode(input: string | string[] | undefined): object | undefined {
      if (input == null || !input.length) {
        return undefined;
      }

      const obj = decodeObject(input);

      return Object.keys(obj).reduce((acc, key) => {
        const value = obj[key];
        const param = objParam[key];

        // is it a QueryParam?
        if (param && param.encode && param.decode) {
          return {
            ...acc,
            [key]: param.decode(value),
          };
        }

        return {
          ...acc,
          [key]: value,
        };
      }, {});
    },
  };
};
const filterDefinition = {
  begin: MomentParam,
  end: MomentParam,
};
const FilterParam = getSchemaParam(filterDefinition);

FilterParam.encode
FilterParam.decode

it also needs to be recursive

nesting solution

import { QueryParamConfig, encodeObject, decodeObject } from 'use-query-params';

const encodeObj = (objParam: QueryParamConfig<any>) => (input: object) => {
  if (objParam && objParam.encode && objParam.decode) {
    return objParam.encode(input);
  }

  const objEncoded = Object.keys(input).reduce((acc, key) => {
    const value = input[key];
    const param = objParam[key];

    // is it a QueryParam?
    if (param && param.encode && param.decode) {
      return {
        ...acc,
        [key]: param.encode(value),
      };
    }

    // nested object param
    if (typeof value === 'object') {
      const encoded = encodeObj({
        [key]: param,
      })(value);

      return {
        ...acc,
        [key]: encoded,
      };
    }

    return {
      ...acc,
      [key]: value,
    };
  }, {});

  return encodeObject(objEncoded);
};

const decodeObj = (objParam: QueryParamConfig<any>) => (input: string | string[] | undefined): object | undefined => {
  if (input == null || !input.length) {
    return undefined;
  }

  if (objParam && objParam.encode && objParam.decode) {
    return objParam.decode(input);
  }

  const obj = decodeObject(input);

  return Object.keys(obj).reduce((acc, key) => {
    const value = obj[key];
    const param = objParam[key];

    // is it a QueryParam?
    if (param && param.encode && param.decode) {
      return {
        ...acc,
        [key]: param.decode(value),
      };
    }

    // nested object param
    if (typeof param === 'object') {
      const decoded = decodeObj(param)(value);

      return {
        ...acc,
        [key]: decoded,
      };
    }

    return {
      ...acc,
      [key]: value,
    };
  }, {});
};

// TODO - improve type
export const getSchemaParam = (objParam: QueryParamConfig<any>): QueryParamConfig<any> => {
  const encode = encodeObj(objParam);
  const decode = decodeObj(objParam);

  return {
    encode,
    decode,
  };
};

tests

import { StringParam, BooleanParam } from 'use-query-params';

import { getSchemaParam } from '../getSchemaParam';

const result = 'quadrant-atende_user-manager-managerId_oneOnOne-hasMadeOne-true';

// TODO - fix nesting
it.skip('should encode complex filter properly using schemaParam', () => {
  const userFilterDefinition = {
    quadrant: StringParam,
    user: {
      manager: StringParam,
      oneOnOne: {
        hasMadeOne: BooleanParam,
      },
    },
  };

  const UserFilterParam = getSchemaParam(userFilterDefinition);

  const filters = {
    quadrant: 'atende',
    user: {
      manager: 'managerId',
      oneOnOne: {
        hasMadeOne: true,
      },
    },
  };

  const encoded = UserFilterParam.encode(filters);

  expect(encoded).toEqual(result);
});

// TODO - fix nesting
it.skip('should decode complex filter properly using schemaParam', () => {
  const userFilterDefinition = {
    quadrant: StringParam,
    user: {
      manager: StringParam,
      oneOnOne: {
        hasMadeOne: BooleanParam,
      },
    },
  };

  const UserFilterParam = getSchemaParam(userFilterDefinition);

  const filters = {
    quadrant: 'atende',
    user: {
      manager: 'managerId',
      oneOnOne: {
        hasMadeOne: true,
      },
    },
  };

  const encoded = UserFilterParam.decode(result);

  expect(encoded).toEqual(filters);
});

tests are still failing