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

@finnair/path

v7.0.0-alpha.9

Published

Simple object path as array of strings and numbers

Downloads

3,658

Readme

Path

@finnair/path contains partly JsonPath compatible path utilities:

  • Path - concrete JSON paths used to locate, read or write a of an object.
  • PathMatcher - a JsonPath like query processor.
  • Projection - PathMatcher based include/exclude mapper for providing partial results from e.g. an API.

Parsers for Path and PathMatcher are available in a separate package @finnair/path-parser.

Getting Started

Install v-validation using yarn:

yarn add @finnair/path

Or npm:

npm install @finnair/path

Use Case Examples

Validation

Path can be used to point a location of invalid value (see v-validation. Path's immutability and fluent API makes it easy and safe to use.

Diff, Versioning & Patching

Analyze changes and trigger logic based on what has changed (see diff.

Include/Exclude Projection

While GraphQL is all about projections, something similar can also be implemented in a REST API with include/exclude parameters. Projection and parsePathMatcher function provides means to process results safely based on such a user input. This is, of course, very simplified projection compared to what GraphQL has to offer, but it's also... well, simpler.

Projection can also be used to optimize fetching expensive relations as it also supports matching Paths and not just mapping actual values:

const resource = fetchResult(request);
const projection = Projection.of(parseIncludes(request), parseExcludes(request));
const result = {
  ...resource,
  veryExpensiveRelation: projection.match(Path.of('veryExpensiveRelation')) ? fetchVeryExpensiveRelation(resource) : undefined,
};
return projection.map(result);

Using Path

Path is an immutable representation of a concrete JsonPath, consisting of strings (properties) and numbers (indexes).

import { Path } from '@finnair/path';

// Constructing Paths
Path.of(); // Root object $ - also Path.ROOT
Path.of('array', 1, 'property'); // $.array[1].property

// Constructing with fluent syntax
Path.of().property('array').index(0); // $.array[0]

// Concatenating Paths
Path.of('parent').concat(Path.of('child', 'name')); // $.parent.child.name
// Or the other way around
Path.of('child', 'name').connectTo(Path.of('parent')); // $.parent.child.name

// Accessing Path components
Array.from(Path.of(1, 2, 3)); // [1, 2, 3] - Path is an Iterable
Path.of('parent', 'child').length; // 2
Path.of('array', 3).componentAt(0); // 'array'
Path.of('array', 3).componentAt(1); // 3

// Getting Path value from an object
Path.of('array', 1).get({ array: [1, 2, 3] }); // 2

// Setting Path value of an object
Path.of('array', 1).set({}, 'foo'); // { array: [undefined, 'foo'] }
Path.of('parent', 'child', 'name').set({}, 'child name'); // { parent: { child: { name: 'child name' } } }

// Unsetting Path value of an object
Path.of('array', 1).unset({ array: [1, 2, 3] }); // { array: [1, undefined, 3] }
// ...but unsetting a value doesn't create intermediate objects
Path.of('array', 1).unset({}); // {}

// Trailing undefined elements will be removed from an array (i.e. array is resized)
Path.of('array', 2).set({ array: [1, undefined, 3] }, undefined); // { array: [1] } where array.length === 1

// toJSON() returns JsonPath compatible serialization
Path.of('array', 0, 'property with spaces').toJSON(); // $.array[0]["property with spaces"]

Parsing Paths

import { parsePath } from '@finnair/path-parser';

parsePath(`$.array[1]["\\"property\\" with spaces and 'quotes'"]`); // JSON string encoded in brackets!
// Path.of('array', 1, `"property" with spaces and 'quotes'`);

// Single quotes also work, but encoding is still JSON string so the value cannot contain ' character
parsePath(`$['single quotes']`); // Path.of('single quotes')
parsePath(`$['single\'quote']`); // Fail!

// ...without using unicode escape
parsePath(`$['\\u0027']`); // Path.of("\'");

Using PathMatcher

PathMatcher is constructed from PathExpression[]. Each PathExpression is capable of handling one path component, find matching values, testing if a (concrete) Path component is a match and serialize the expression to string. As PathExpression is just a simple interface, it is possible also to implement custom PathExpressions, however, the default parser cannot handle them of course.

Constructing PathMatcher

import { PathMatcher, AnyIndex, AnyProperty, UnionMatcher } from '@finnair/path';

// Constructing using static creator - string is shorcut for PropertyMathcer and number for IndexMatcher
PathMatcher.of('array', AnyIndex, 'name'); // $.array[*].name
PathMatcher.of(AnyProperty, 'length'); // $.*.length
PathMatcher.of('child', UnionMatcher.of('name', 'value')); // $.child['name','value']

Finding values

find returns Nodes of path and value

PathMatcher.of('array', AnyIndex).find({ array: [1, 2], other: 'property' }); // $.array[*]
// [ { path: Path.of('array', 0), value: 1 }, { path: Path.of('array', 1), value: 2 } ]

findValues returns actual values

let array: any = [1, 2];
array.property = 'stupid thing to do';
PathMatcher.of(AnyProperty).findValues(array)); // [1, 2, 'stupid thing to do']
PathMatcher.of(AnyIndex).findValues(array); // [1, 2]

// ...also undefined array elements
array = [];
array[2] = 'first actual value';
PathMatcher.of(AnyIndex).findValues(array); // [undefined, undefined, 'first actual value']

Finding first match and value is also directly supported

// Finding first, possibly undefined, match
PathMatcher.of(AnyIndex).findFirst([undefined, 2, 3]);
// [ { path: Path.of(0) } ]

// Finding first value doesn't make difference between "nothing found" and "found undefined value"
PathMatcher.of(AnyIndex).findFirst([undefined, 2, 3]); // undefined
PathMatcher.of(4).findFirst([]); // undefined

Matching Paths

Sometimes it's usefull to be able to also match Paths directly against a PathMatcher...

// Exact match
PathMatcher.of(AnyProperty).match(Path.of('parent')); // true
PathMatcher.of(AnyProperty).match(Path.of('parent', 'child')); // false
PathMatcher.of(AnyProperty).match(Path.of()); // false

// Prefix match
PathMatcher.of(AnyProperty).prefixMatch(Path.of('parent')); // true
PathMatcher.of(AnyProperty).prefixMatch(Path.of('parent', child)); // true
PathMatcher.of(AnyProperty).prefixMatch(Path.of()); // false

// Partial match - prefix of suffix
PathMatcher.of(AnyProperty).partialMatch(Path.of('parent')); // true
PathMatcher.of(AnyProperty).partialMatch(Path.of('parent', child)); // true
PathMatcher.of(AnyProperty).partialMatch(Path.of()); // true
PathMatcher.of('parent', 'one').partialMatch(Path.of('parent', 'two')); // false

toJSON

PathMatcher.toJSON() returns JsonPath like representation of the matcher. Main difference is that bracket–notation (when required) uses JSON string encoding.

Parsing PathMatchers

parsePathMatcher parses simple JsonPath like expressions. Supported expressions are

| Expression | Description  | | ----------------------------------- | --------------------------------------------------------------------- | | $.property | Identifiers matching RegExp /^[a-zA-Z_][a-zA-Z0-9_]*$/ | | $[0] | Index match | | $.* | Any property matcher, wildcard (matches also array indexes) | | $[*] | Any index matcher, wildcard (matches only array indexes) | | $["JSON string encoded property"] | Property as JSON encoded string | | $['JSON string encoded property'] | Property as single quoted, but otherwise JSON encoded, string(*) | | $[union,"of",4,'components'] | Union matcher that also supports identifiers and JSON encoded strings |

*) This is the official way of JsonPath, but the specification is a bit unclear on the encoding. In this library we prefer proper JSON string encoding with double quotes.

import { parsePathMatcher } from '@finnair/path-parser';

parsePathMatcher(`$.array[0][*].*['union',"of",properties,1]`);
// PathMatcher.of(
//   'array',
//    0,
//   AnyIndex,
//   AnyProperty,
//   UnionMatcher.of('union', 'of', 'properties', 1)
// )

Using Projection

Projection is a collection of include and exclude PathMatchers. It's main use is to map a projection of it's input based on the include/exclude configuration. It also allows matching Path instances directly.

const example = {
  name: 'name',
  array: [
    { name: 'one', value: 1 },
    { name: 'two', value: 2 },
  ],
};

// Only includes
Projection.of([PathMatcher.of('array')]).map(example);
// { array: [ { name: 'one', value: 1 }, { name: 'two', value: 2 } ] }

// Only excludes
Projection.of([], [PathMatcher.of('array')]).map(example);
// { name: 'name' }

// Includes and excludes
Projection.of([PathMatcher.of('array')], [PathMatcher.of('array', AnyIndex, 'name')]).map(example);
// { array: [{ value: 1 }, { value: 2 }] }

// With "compression" of arrays
Projection.of([PathMatcher.of('array', 1, 'name')]);
// { array: [{ name: 'two' }] }

Projection.map does not modify it's input, but returns a "JSON clone" - except in a case when there are neither includes nor excludes, in which case it returns the input object directly.

JSON clone is also essential for security reasons as it prevent's a malicious user from accessing internals of an object (e.g. Moment).

Projection can also match Path instances directly, which can be used for example as an optimization for skipping fetching of an expensive relation.

if (projection.match(Path.of('array', AnyIndex, 'name'))) {
  fetchNamesFor(result.array):
}

Why Yet Another "JsonPath" Library?

Gave up trying to find a library that satisfies all our requirements ¯\(ツ)

  • Security: JsonPath contains parts that are strictly NOT safe for handling untrusted user input (e.g. static-eval#security)
  • Encoding of bracket notation properties
    • Bracket notation property encoding is not defined by the JsonPath specification
    • JSON string encoding seems a logical choice, but that conflicts with single quote being the selected quote type for property names as there is no escape sequence for single quote in JSON string encoding
    • While parser also supports single quotes, this library uses JSON string encoding with double quotes by default
  • Clear separation of concrete paths and matchers
    • Concrete paths are required by v-validation to point to a location of an invalid value
    • Matchers are required for example by an API for include/exclude functionality
  • Clear separation of logic and data sturctures from parsers
  • Functions are especially designed to fit our use cases