Search code examples
javascriptarraysdata-structuresfilteringmultiple-conditions

How does one create custom filter conditions for array items upon ever newly computed query-data?


I have a filter object that is returned by query params

url = /all?channels=calls,text&calls=voicemail,missed

const query = {
  channels: 'calls,texts',
  calls: 'voicemail,missed',
};

I then have an array of objects that come in from a socket.

const arr = [
  {
    id: 1,
    channel: 'SMS',
    sent: '2021-08-22T03:21:18.41650+0000',
    sender: {
      contactType: 'business',
    },
    recipients: [
      {
        contactType: 'corporate',
      },
    ],
    direction: 'INBOUND',
  },
  {
    id: 2,
    channel: 'VOICE',
    sent: '2021-08-20T23:15:56.00000+0000',
    sender: {
      contactType: 'business',
    },
    recipients: [
      {
        contactType: 'corporate',
      },
    ],
    callDetails: {
      answered: false,
      voicemail: true,
    },
    direction: 'INBOUND',
  },
  {
    id: 3,
    channel: 'VOICE',
    sent: '2021-08-20T23:15:56.00000+0000',
    sender: {
      contactType: 'business',
    },
    recipients: [
      {
        contactType: 'corporate',
      },
    ],
    callDetails: {
      answered: true,
      voicemail: false,
    },
    direction: 'INBOUND',
  },
  {
    id: 4,
    channel: 'VOICE',
    sent: '2021-08-20T23:15:56.00000+0000',
    sender: {
      contactType: 'business',
    },
    recipients: [
      {
        contactType: 'corporate',
      },
    ],
    callDetails: {
      answered: false,
      voicemail: false,
    },
    direction: 'INBOUND',
  },
];

I want to filter out the objects that match the filters but the query obj isn't friendly enough to just map the arr through.

With the query obj shared above, i should return the objects id:1 and id:2 and id:4 from arr, since those object meet the criteria of sms, voicemail, & missed

I assume i need a modified query obj that has to have various conditions available for each property, i.e calls: voicemail === callDetails.voicemail === true or calls: received === callDetails.answered === true

I've seen lots of examples on how to filter an array of objects with multiple match-criteria, but with the req of the property having multiple conditions, i've hit a wall.

thanks for the help


Solution

  • The main idea is to provide kind of a rosetta stone which does bridge/map the query specific syntax with any list item's specific data structure. Thus one will end up writing a map which takes a query's structure into account but ensures for each necessary query endpoint an item specific filter condition/function.

    The query function should simply filter the item list by applying a list of logical OR conditions, thus using some for returning the boolean filter value.

    Which leaves one of implementing a helper method which collects ... via Object.entries and Array.prototype.flatMap as well as via String.prototype.split and Array.prototype.map ... the function endpoints from the above introduced requirements configuration/map, based on the query object, provided by the system. Thus this helper might be named resolveQuery.

    const sampleList = [{
      id: 1,
      channel: 'SMS',
    
      direction: 'INBOUND',
    }, {
      id: 2,
      channel: 'VOICE',
    
      callDetails: {
        answered: false,
        voicemail: true,
      },
      direction: 'INBOUND',
    }, {
      id: 3,
      channel: 'VOICE',
    
      callDetails: {
        answered: true,
        voicemail: false,
      },
      direction: 'INBOUND',
    }, {
      id: 4,
      channel: 'VOICE',
    
      callDetails: {
        answered: false,
        voicemail: false,
      },
      direction: 'INBOUND',
    }];
    
    // prepare a `requirements` map which ...
    // - on one hand maps `query`-syntax to a list items's structure
    // - and on the other hand does so by providing an item specific
    //   filter condition/function for each necessary query endpoint.
    const requirements = {
      channels: {
        texts: item => item.channel === 'SMS',
      },
      calls: {
        voicemail: item => item.channel === 'VOICE' && !!item.callDetails.voicemail,
        missed: item => item.channel === 'VOICE' && !item.callDetails.answered,
      },
    }
    // const query = {
    //   channels: 'calls,texts',
    //   calls: 'voicemail,missed',
    // };
    
    function resolveQuery(requirements, query) {
      const reject = item => false;
    
      // create/collect a list of filter condition/functions
      // which later will be applied as logical OR via `some`.
      return Object
    
        .entries(query)
        .flatMap(([ groupKey, groupValue ]) =>
          // e.g groupKey => 'channels',
          // groupValue => 'calls,texts'
          groupValue
            .split(',')
            .map(requirementKey =>
              // e.g requirementKey => 'calls'
              // or requirementKey => 'texts'
              requirements?.[groupKey]?.[requirementKey?.trim()] ?? reject
            )
        );
    }
    
    function queryFromItemList(itemList, requirements, query) {
      const conditionList = resolveQuery(requirements, query);
    
      console.log(
        'conditionList ... [\n ',
        conditionList.join(',\n  '),
        '\n]'
      );
    
      return itemList.filter(item =>
        conditionList.some(condition => condition(item))
      );
    }
    
    console.log(
      queryFromItemList(sampleList, requirements, {
        channels: 'calls,texts',
        calls: 'voicemail,missed',
      })
    );
    .as-console-wrapper { min-height: 100%!important; top: 0; }