npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

jsxpath

v1.1.5

Published

XPath like notation to query a JSON object

Downloads

157

Readme

JSXPath

JSXPath is an adaptation of XPath, a querying language for XML documents, to query JSON object.

If you are already familiar with the construct of XPath, using this should be a breeze.

version

Latest version: v1.1.5

Why JSXPath?

  1. Powerful, yet simple to use to perform complex query on JSON data with just a string expression.
  2. Packed full of features out of the box.
  const json = { "a": 1, "b": 2, "c": "pass"}

  /*----------
  without JSXPath
  ----------*/
  function sum(pa, pb) {
    if (!isNumber(pa) || isNumber(pb)) {
      throw new Error("an argument is not a number");
    }
    return pa + pb;
  }

  function isNumber(num) {
    return !isNaN(num) && isFinite(num);
  }

  const result = sum(js.a, js.b) === 3 ? js.c : null;
  // result => 'pass'


  /*----------
  with JSXPath
  ----------*/
  import { runPath } from 'jsxpath';

  const result = runPath('/c[sum(/a | /b) = 3]', { json });
  // result => ['pass']

INSTALL

npm install jsxpath

USE

  const json = {
    a: {
        links: [
          { id: 3, type: 'b' },
          { id: 1, type: 'c' }
        ],
        value: 'master'
    },
    b: [
      { id: 1, value: 'one' },
      { id: 2, value: 'two' },
      { id: 3, value: 'three' }
    ]
  };
  import { runPath } from 'jsxpath';

  // Get the value of "b" that is linked back to "a" by "id"
  const path = '/b/*[id = /a/links/*[type="b"]/id]/value';

  const result = runPath(path, {json}); // ["three"]


  // callback version
  //-------------------------
  runPath({
    path: path,
    then: ({ path, error, value }) => {
      // value => ['three'];

      // run your custom code
    } 
  }, {json});


  // path looks complicated? 
  // use variables.
  //-------------------------
  const result = runPath( '/b/*[id = $aLinkToB_id]/value', 
    { 
      json, variables: { aLinkToB_id: '$root/a/links/*[type="b"]/id' } 
    }
  ); // ["three"]

Evaluating multiple paths within one run


  const json = {
    c: [
      { id: 1, type: 'TypeA', value: 'car'},
      { id: 2, type: 'TypeB', value: 'house'},
      { id: 3, type: 'TypeA', value: 'boat'}
    ]
  }
  import { runPaths } from 'jsxpath';

  const pathsResult = {
    entities: null,
    typeAs: null
  };

  const pathsAndCallbacks: tPathWithCallBack[] = [
    { 
      path: '/c/*/value',
      then: (result) => {
        // custom code
        if (result.value.length) {
          pathsResult.entities = result.value;
        }
      }
    },
    {
      path: '/c/*[type="TypeA"]',
      description: 'type "A" objects in "c"',
      then: ({value, error}) => {
        // custom code
        if (!error) {
          pathsResult.typeAs = value;
        }
      }
    }
  ];

  runPaths( pathsAndCallbacks, { json });

  /*
  * pathsResult => {
  *   entities: ['car', 'house', 'boat'],
  *   typeAs: [
  *     { id: 1, type: 'TypeA', value: 'car'},
  *     { id: 3, type: 'TypeA', value: 'boat'}
  *   ]
  * }
  */

API

These are the 3 methods to run the path expression

  runPath( path: string, inputProps: tRunPathsInput )
  1. path: path expression
  2. inputProps: tRunPathsInput
type tRunPathsInput {
  json: object,
  functions?: {
    [functionName: string]: (...args: tStack[]) => tStack
  },
  variables?: {
    [variableName: string]: any
  }
};
  runPath( pathProp: tPathWithCallBack, inputProps: tRunPathsInput )
  1. pathProp: tPathWithCallBack
  • path: path expression
  • description?: describe what this path expression is about
  • then: a callback function that accepts the result of executing the path expression.
type tPathWithCallBack = {
  path: string,
  description?: string,
  active?: boolean,
  then(result: tRunPathResult),
  [key: string]: any
};

type tRunPathResult = {
  path: string,
  value: any,
  error?: string
};
  1. inputProps: tRunPathsInput
  • json: the json object to interrogate
  • functions?: see custom functions
  • variables?: see variables
  • outputOptions?: flags to determine extra optionally return values
type tRunPathsInput {
  json: object,
  functions?: {
    [functionName: string]: (...args: tStack[]) => tStack
  },
  variables?: {
    [variableName: string]: any
  },
  outputOptions?: {
    nodes?: boolean
  }
};
  runPaths( pathProp: tPathWithCallBack[], inputProps: tRunPathsInput )
  1. pathProp: tPathWithCallBack[]
  • path: path expression
  • description?: describe what this path expression is about
  • then: a callback function that accepts the result of executing the path expression.
type tPathWithCallBack = {
  path: string,
  description?: string,
  active?: boolean,
  then(result: tRunPathResult),
  [key: string]: any
};

type tRunPathResult = {
  path: string,
  value: any,
  error?: string
};
  1. inputProps: tRunPathsInput
  • json: the json object to interrogate
  • functions?: see custom functions
  • variables?: see variables
  • outputOptions?: flags to determine extra optionally return values
type tRunPathsInput {
  json: object,
  functions?: {
    [functionName: string]: (...args: tStack[]) => tStack
  },
  variables?: {
    [variableName: string]: any
  },
  outputOptions?: {
    nodes?: boolean
  }
};

Differences & Limitations

There are some notable differences and limiations between xml and json that the query langauge do not support.

  • The '@' symbol is not used in JSXPath expression since JSON only consists of key value pair. '@' in XML denotes an attribute.
  • The axis 'preceding', 'preceding-sibling', 'following', and 'following-sibling' is not supported. JSON is a hash map, the keys are not always returned in a particular order. Instead JSXPath supports 'sibling' that looks for key value within the same object.
  • The operator token keywords are reserved. This means that the keys in the json cannot contain the following symbols (|,/,+, -, %, *, =, >, <) and spaces

FEATURES

Operators

| Operators | | |---|---| | | | Unary | | + | Addition | | - | Subtraction | | * | Multiplication | | div | Division | | = | Equal | | != | Not Equal | | < | Less Than | | <= | Less Than or Equal to | | > | Greater Than | | >= | Greater Than or Equal to | | or | Or | | and | And | | mod | Modulus |

Axes

| Select | | |---|---| | . | Current | | .. | Parent from Current | | / | Root or Child from Current | | // | Descendants from Current | | * | Any child nodes from Current | | parent | Parent from Current | | ancestor::(node-name) | Ancestors from Current | | ancestor-or-self::(node-name) | Ancestors + Current from Current | | child::(node-name) | Child from Current | | descendant::(node-name) | Descendants from Current | | descendant-or-self::(node-name) | Descendants + Current from Current | | sibling::(node-name) | Sibling of Current (key name within the same object)|

Functions

Built in functions

| Name | Example | Result | Comment | |--|--|--|--| | abs | path = "abs(-1)" | 1 | | boolean | path = 'boolean("string")' | true | if arg is node list, returns true if list is not empty | | ceiling | path = 'ceiling(1.2)' | 2 | | | choose | path = 'choose(1=1, "abc", "def")' | "abc" | if first arg evaluates to true, return 2nd arg otherwise return 3rd arg | | concat | path= 'concat("ab", " ", "cd", ": ", 1, " is ", true)' | "ab cd: 1 is true" | Converts number or boolean type to a string, if it is a node type will interrogate and return the value of the first node item | contains | path = 'contains("needle haystack", "hay")' | true | | | count | path = 'count(//a)' | count the number of nodes with key value of "a" starting from root node | | | false | path = 'false = false()' | true | | first | path = '/a/[first()]' | first() returns the first position of the node list | index is 1 base | | floor | path = 'floor(1.2)' | 1 | | last | path = '/a/[last()]' | last() returns the last position of the node list | index is 1 base | | local-name | path = '//[local-name() = "abc"]' | The key name of the node | see example | name | path = '//[name() = "abc"]' | equivalent to local-name | | not | path = '/a/[not(b > 1)]' | return all child nodes of a whose b value is less than or equal to 1 | | number | path = 'number(/a)' | return the number value of the first node of a | if it's a string or boolean type, it will try to convert it to a number value, otherwise return NAN. Throws an error if the passed in type is not a string, number, or boolean. | round | path = 'round(4.4)', 'round(4.5)' | 4 , 5 | round the number to the nearest integer value | string | path = 'string(1.1)' | '1.1' | convert and return a string value. Throws an error if the passed in type is not a string, number, or boolean. | | substring-after | path = 'string-after("haystack", "st") | "ack" | | | substring-before | path = 'string-before("haystack", "st") | "hay" | | | sum | path = 'sum(/a//b)' | sum all value of b | Throws an error if the value is not node and is of number type | | true | path = 'true = true()' | true | |

Examples

  import {runPaths} from 'jsxpath';

  const budget = {
    incomes: [
      { id: 1, type: 'salary', display: 'salary', value: 2000.30, frequency: 'monthly'},
      { id: 2, type: 'rent', display: 'rent', value: 300.95, frequency: 'fortnightly'},
      { id: 3, type: 'share', display: 'shares', value: 0.20, frequency: 'monthly'}
    ],
    expenses: [
      { id: 1, type: 'transport', display: 'car', value: -200.70, frequency: 'fortnightly' },
      { id: 2, type: 'household', display: 'grocery', value: -400.20, frequency: 'monthly' },
      { id: 3, type: 'transport', display: 'train', value: -200.10, frequency: 'monthly' },
      { id: 4, type: 'household', display: 'gardening', value: -20.10, frequency: 'monthly' }
    ]
  }
  /* 
  * evaluate a series of paths to extract required expenses
  * and incomes to calculate the net income per month
  */
  let totalIncomePerMonth = 0;
  runPaths([
    {
      path: 'sum(/incomes/*[frequency="monthly"][floor(value) >= 1]/value)',
      then: ({value}) => {
        // returns salary value
        totalIncomePerMonth += value;
      }
    },
    {
      path: 'sum(/incomes/*[frequency="fortnightly"]/value) * 2',
      then: ({value}) => {
        // returns rent value
        totalIncomePerMonth += value;
      }
    },
    {
      path: 'sum(/expenses/*[frequency="monthly"][abs(value) > 25]/value)',
      then: ({value}) => {
        // returns grocery and train sum value
        totalIncomePerMonth += value;
      }
    },
    {
      path: 'sum(/expenses/*[frequency="fortnightly"]/value) * 2',
      then: ({value}) => {
        // returns car value
        totalIncomePerMonth += value;
      }
    }
  ], {json: budget});

  // totalIncomePerMonth = 1600.5

Other function examples

  runPaths([
    {
      path: '/incomes/*[first()]/display',
      description: 'first() eg',
      then: ({value}) => {
        // value => ['salary']
      }
    },
    {
      path: '/incomes/*[last()]',
      description: 'last() eg',
      then: ({value}) => {
        // value => [{ id: 3, type: 'share', display: 'shares', value: 0.20, frequency: 'monthly'}]
      }
    },
    {
      path: 'count(/incomes/*[frequency = "monthly"])',
      description: 'count() eg',
      then: ({value}) => {
        // value => 2;
      }
    },
    {
      path: '//*[local-name()="id"][sibling::type = "transport"]',
      description: 'local-name() eg',
      then: ({value}) => {
        // value => [1, 3];
      }
    },
    {
      path: '/expenses/*[concat(type, ":", display) = "transport:train"]/value',
      description: 'concat() eg',
      then: ({value}) => {
        // value => [-200.10]
      }
    }
  ], { json: budget });

Custom functions

JSXPath supports the ability for you to write your own custom functions and be able to refer to it in the path expression. It is passed into the runPath or runPaths as part of the second argument and has the signature of:

  type tRunPathsInput {
    json: object,
    functions?: {
      [functionName: string]: (...args: tStack[]) => tStack
    },
    ...
  };

Functions accepts a list of tStack arguments and expected to return a tStack value

  // for the purpose of functions, we would expect 
  // tStack to typically have the following definition
  type tStack {
    type: 'nodes' | 'boolean' | 'string' | 'number', 
    value: tNode[] | boolean | string | number
  };

Example: custom function to get maximum value in a list of nodes

  import { runPath, KEYS } from './index';

  const json = {
    a: [
      {value: 3},
      {value: '90'},
      {value: 16}
    ],
    b: [
      {value: -1},
      {value: 30},
      {value: true}
    ]
  };

  // defining custom functions
  functions = {
    max: (item: tStack): tStack => {
      if (item.type !== 'nodes') {
        throw new Error('[functions.max], invalid arg type. Was expecting nodes');
      }
      // loop through the node list, check if the type is a number
      // and has a value greater than the current max value
      const value = item.value.reduce((maxNumber, node) => {
        if (node[KEYS.valueType] === 'number' && node[KEYS.value] > maxNumber) {
          maxNumber = node[KEYS.value];
        }
        return maxNumber;
      }, 0);
      
      return { type: 'number', value };
    },
    //... more functions
  };

  const maxPlus10 = runPath('max( /a/*/value | /b/*/value ) + 10', { json, functions });
  // maxPlus10 => 40

Refer to about JSXPath nodes to see how JSXPath converts JSON object into nodes.

Variables

A powerful!! feature to link to another value that can be used as part of path expressions.

  • Variables in path expression starts with $ sign followed by the name of the key passed in the variable object.
  • Variable does not have to be actual values, it can also be a path expression in itself.

Consider the budget example set up in the functions section above. We can simplify it using variable paths expressions.

  import {runPath} from 'jsxpath';

  const budget = {
    incomes: [
      { id: 1, type: 'salary', display: 'salary', value: 2000.30, frequency: 'monthly'},
      { id: 2, type: 'rent', display: 'rent', value: 300.95, frequency: 'fortnightly'},
      { id: 3, type: 'share', display: 'shares', value: 0.20, frequency: 'monthly'}
    ],
    expenses: [
      { id: 1, type: 'transport', display: 'car', value: -200.70, frequency: 'fortnightly' },
      { id: 2, type: 'household', display: 'grocery', value: -400.20, frequency: 'monthly' },
      { id: 3, type: 'transport', display: 'train', value: -200.10, frequency: 'monthly' },
      { id: 4, type: 'household', display: 'gardening', value: -20.10, frequency: 'monthly' }
    ]
  }
  const totalIncomePerMonth = runPath(
    '$incomeMonthlyTypes + $incomeFortnightlyTypes + $expenseMonthlyTypes + $expenseFortnightlyTypes', 
    {
      json: budget,
      variables: {
        incomeMonthlyTypes: 'sum($root/incomes/*[frequency="monthly"][floor(value) >= 1]/value)',
        incomeFortnightlyTypes: 'sum($root/incomes/*[frequency="fortnightly"]/value) * 2',
        expenseMonthlyTypes: 'sum($root/expenses/*[frequency="monthly"][abs(value) > 25]/value)',
        expenseFortnightlyTypes: 'sum($root/expenses/*[frequency="fortnightly"]/value) * 2'
      }
    }
  );
  
  // totalIncomePerMonth = 1600.5

Note that on initialization, the passed in json is stored as a variable with the name $root. $root can be referenced in both the main path or variable path expression

Example below shows additional ways of using variables

  • tuitionType is a literal object value
  • tuitionAmount is a path expression referencing $tuitionType
  • $tuitionAmount is referenced in the main path expression
  runPath({
    path: 'abs($tuitionAmount) > abs(/expenses/*[display="gardening"]/value)',
    description: 'Is tuition fee cost more than the amount spent on gardening?',
    then: ({value}) => {
      // custom code here
      const message = value === true ?
        'Tuition is overly expensive. We should spend our time on gardening':
        'Tuition is still dirt cheap.'
      console.log(message);
    }
  }, {
    json: budget,
    variables: {
      tuitionType: { id: 100, type: 'education', display: 'tuition', value: -80, frequency: 'monthly' },
      tuitionAmount: '$tuitionType/value'
    }
  });

  // console.log => 'Tuition is overly expensive. We should spend our time on gardening'

About JSXPath nodes

In JSXPath, we natively deconstruct the passed in JSON object into a series of nodes in order to make it easier to traverse and perform a deep comparisons without relying on third party libraries. Before returning the result, JSXPath will reconstruct the filtered down node(s) as an array of JSON object.

Node have the following shape

  type tNode = [
    id: number,
    depth: number,
    group: number|string,
    arrayPosition: number|string,
    key: string,
    value: any,
    valueType: string,
    links: {
      parentId?: number,
      childrenIds?: number[],
      descendantIds?: number[],
      ancestorIds?: number[],
      siblings?: number[]
    }
  ];

Given the passed in JSON is

  {
    a: [
      0,
      {b: 'c', d: 1},
      'efg'
    ]
  }

it is deconstructed into a list of array nodes and linked together as shown below diagramatically.

classDiagram
root --> a : child/parent
a --> 0: child/parent
a --> `$ao`: child/parent
b <..> d: sibling
`$ao` --> b: child/parent
`$ao` --> d: child/parent
a --> efg: child/parent

class root {
  + id: 0
  + key: $r
  + value: $o
  + valueType: object
}
class a {
  + id: 1
  + key: a
  + value: $a
  + valueType: array
}
class 0 {
  + id: 2
  + key: $v
  + value: 0
  + valueType: number
}

class `$ao`["$ao (object in an array)"] {
  + id: 3
  + key: $ao
  + value: $o
  + valueType: object
}

class b {
  + id: 4
  + key: b
  + value: c
  + valueType: string
}
class d {
  + id: 5
  + key: d
  + value: 1
  + valueType: number
}
class efg {
  + id: 6
  + key: $v,
  + value: efg,
  + valueType: string
}

apart from child/parent/sibling relationship, each node have links to ancestors and descendants

Helper functions are provided that can be used in custom functions

  import { KEYS, nodesOps } from 'jsxpath';

  // Actual node properties can be accessed via the KEYS object
  nodeA[KEYS.valueType] //object, array, string,...
  nodeA[KEYS.value]
  nodeA[KEYS.links].childrenIds // [1, 4, ...]

  // Get the linked nodes
  nodesOps.get.ancestors(currentNode: tNode, nodeName?: string): tNode[];
  nodesOps.get.children(currentNode: tNode, nodeName?: string): tNode[];
  nodesOps.get.descendants(currentNode: tNode, nodeName?: string): tNode[];
  nodesOps.get.parent(currentNode: tNode, nodeName?: string): tNode;
  nodesOps.get.siblings(currentNode: tNode, nodeName?: string): tNode[];

  // convert the node into its' JSON value
  nodesOps.reconstruct(nodes: tNode[]): (iJSONArray|tJSONObject)[];

  // test to see if two nodes are equal (deep)
  nodesOps.test.isEqual(node1: tNode, node2: tNode): boolean;

ROADMAP

  • better error handling overall
  • introduce date functions