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

gotjs

v0.0.10-1

Published

got is a fluent, functional, zero-dependency, in-memory JavaScript graph database. You can create data structures at any degree of complexity within your JavaScript application both in the browser and Node.js. The whole database is represented in a state

Downloads

108

Readme

got

got is a fluent, functional, zero-dependency, in-memory JavaScript graph database. You can create data structures at any degree of complexity within your JavaScript application both in the browser and Node.js. The whole database is represented in a state object which is basically a plain JavaScript object. The state is immutable, which means that existing states are never modified directly, instead a mutation will always result in a clone of the previous state, including the updates. For that reason, got is also perfect to manage heavily complex states with pure Redux.

inspired by: Ramda, Gun, Redux, GraphQL, Cycle.js and git 😋

Installation

On Node:

$ npm i gotjs or

$ yarn add gotjs

The package provides its own typings so you can use it with TypeScript out of the box. However you can also start over with plain JS. For the sake of simplicity all examples in this README are written in JS (ES2015).

Usage

As any other graph database a got graph consists of exact two entities: nodes and edges. Nodes have a property id which uniquely identifies them in the whole graph. Additionally each node can contain an arbitrarily structured JavaScript object. Edges are defined by at least four properties from, fromType, to and toType. from and to are ids of two nodes which are connected by the edge which would practically be enough to define a very simple graph. But in order to represent any possible data structure we will need to define the type of connection between two nodes. In other words, we have to define the role that one node plays in connection with another. This can be done with the properies fromType and toType.

Writing the Graph State

Now since we know the basic elements of a got graph we should start writing the graph to have a foundation to play with.

Let's model a friendship between John and Paul:

FIGURE: simple graph John <-> Paul

And in the code:

const { got } = require('gotjs');

const friends = got()
    .node({ id: 'person1', name: 'John' })
    .node({ id: 'person2', name: 'Paul' })
    .edge({
        from: 'person1',
        fromType: 'friend',
        to: 'person2',
        toType: 'friend'
    });

console.log(friends.state());

// prints:
// { nodes:
//    { person1: { id: 'person1', name: 'John' },
//      person2: { id: 'person2', name: 'Paul' } },
//   edges:
//    [ { from: 'person1',
//        fromType: 'friend',
//        to: 'person2',
//        toType: 'friend' } ] }

Thats it! Now you have the basic building block for creating every graph that you could think of. The output shows that there is no magic and no hidden information in the state - only the nodes and the edge that we created before. Now you also know the complete set of write operations for got: .node() and .edge().

Note that the initial invoke of got() and all chained write operations (.node() and .edge()) always return a got operator with the cloned graph being able to again perform all operations on its copy of the graph.

Some of you might ask where the benefit is, when the input is as verbose as the output. The reason is that got wants to give you full responsibility for your data - meaning you can create for instance edges for nodes that don't even exist and vice versa. Therefore creating secure shortcuts is also under your responsibility. You can do so by creating custom sugar functions for repetetive tasks of your business logic:

function addFriend(fromFriend, toFriend, gotOperator) {
    return gotOperator
        .node(toFriend)
        .edge({
            from: fromFriend.id,
            fromType: 'friend',
            to: toFriend.id,
            toType: 'friend'
        });
}

You can hide more complex logic in your custom functions to connect for example multiple nodes or work with collections. There are no limits.

You might also notice that this function returns a got operator containing an immutable copy of the state after the two write operations. So all the heavy users of Redux should recognize the pattern: This is nothing more than a reducer function which is taking some arguments plus the old state (wrapped in the gotOperator) and returning a transformed state that did not mutate the old state at all. We will later learn in detail how to integrate got with Redux.

Now you can use it to quickly add friends:

const { got } = require('gotjs');

const john = { id: 'person1', name: 'John' };
const paul = { id: 'person2', name: 'Paul' };
const george = { id: 'person3', name: 'George' };
const ringo = { id: 'person4', name: 'Ringo' };

const friends = got().node(john);
const friends1 = addFriend(john, paul, friends);
const friends2 = addFriend(john, george, friends1);
const friends3 = addFriend(john, ringo, friends2);

console.log(friends3.state());

// prints:
// { nodes:
//    { person1: { id: 'person1', name: 'John' },
//      person2: { id: 'person2', name: 'Paul' },
//      person3: { id: 'person3', name: 'George' },
//      person4: { id: 'person4', name: 'Ringo' } },
//   edges:
//    [ { from: 'person1',
//        fromType: 'friend',
//        to: 'person2',
//        toType: 'friend' },
//      { from: 'person1',
//        fromType: 'friend',
//        to: 'person3',
//        toType: 'friend' },
//      { from: 'person1',
//        fromType: 'friend',
//        to: 'person4',
//        toType: 'friend' } ] }

See how our function did correctly create all the nodes and edges for us - awesome. The reassignments of the operations to friends1, friends2 and friends3 (and feeding them into the next state transition) illustrate how the different steps of state mutation have no impact on previous states:

console.log(friends1.state().nodes.person1);
// prints: { id: 'person1', name: 'John' }

console.log(friends1.state().nodes.person3);
// prints: undefined

console.log(friends2.state().nodes.person3);
// prints: { id: 'person3', name: 'George' }

But don't be afraid that your memory gets spilled over after tons of operations. Even though the modified parts of the state are cloned, the unchanged parts still share the same references:

const person1FirstState = friends1.state().nodes.person1;
const person1SecondState = friends2.state().nodes.person1;
console.log(person1FirstState === person1SecondState);
// prints: true

This is a very basic implementation of the idea of structural sharing.

The above concepts come as heavily simplified examples. Of course you can apply concepts like currying the addFriend(...) function or piping multiple state transitons. There are no boundaries to your creativity in functional programming:

const { got } = require('gotjs');
const R = require('rambda');

// Provide a curried version of addFriend
const addFriend = (fromFriend, toFriend) => (gotOperator) => gotOperator
    .node(toFriend)
    .edge({
        from: fromFriend.id,
        fromType: 'friend',
        to: toFriend.id,
        toType: 'friend'
    });

const john = { id: 'person1', name: 'John' };
const paul = { id: 'person2', name: 'Paul' };
const george = { id: 'person3', name: 'George' };
const ringo = { id: 'person4', name: 'Ringo' };

const johnsGraph = got().node(john);

// Pipe mutation operations together and execute them on johnsGraph
const johnsFriends = R.pipe(
    addFriend(john, paul),
    addFriend(john, george),
    addFriend(john, ringo)
)(johnsGraph);

console.log(johnsFriends.state());

// prints the same state as before

Reading the Graph State

Now since we learned how to write a got graph we should take a look on how to read from the graph. Of course by reading we don't mean to simply call johnsFriends.state() and look at the output. By reading we mean to call query operations on the got operator in a functional fashion so that we can make use of plain JS stuff like .map(), .filter() or .reduce().

To do so the got operator offers us one read operation .lens(id) which is heavily inspired by Ramda however it is implemented a little different plus it is only providing .view() and no .set().

But what is a lens? - for those who haven't heard about Ramda. A lens 🔎 can be described as a small window that is pointing at a node in the graph ready to perform more operations on the node it is pointing at. A lens will give you four functions for selecting nodes in the graph: .view(), .list(type), .first(type) and .prop(name).

First we set up a small example from the previous examples:

const { got } = require('gotjs');

const friends = got({
    nodes: {
        person1: { id: 'person1', name: 'John' },
        person2: { id: 'person2', name: 'Paul' },
        person3: { id: 'person3', name: 'George' },
        person4: { id: 'person4', name: 'Ringo' }
    },
    edges: [{
        from: 'person1',
        fromType: 'friend',
        to: 'person2',
        toType: 'friend'
    },
    {
        from: 'person1',
        fromType: 'friend',
        to: 'person3',
        toType: 'friend'
    },
    {
        from: 'person1',
        fromType: 'friend',
        to: 'person4',
        toType: 'friend'
    }]
});

Note that you can feed an existing state into got(state?) to initialize it. That helps for example in Redux when you write a reducer and receive the previous state as a plain object.

lens(id).view()

Let's start with the most straight forward function .view() which is just returning the object the lens is pointing at:

const person2 = friends.lens('person2').view();
console.log(person2);

// prints: { id: 'person2', name: 'Paul' }

This is again the easiest element of reading the graph which you can later use to compose more complex queries. The lens also acts as an entry point for reading the graph. Let's therefore call the node it is pointing at the entry node of the lens.

lens(id).list(type)

The next important building block is to fetch a list of nodes that is connected to the entry node via a certain type of edge. To view for example all friends of a person we can use .list('friend'):

const johnsFriends = friends.lens('person1').list('friend');

When you print johnsFriends you will notice, that it is not actually holding the nodes but the lenses that point at the nodes. But the cool thing is, that we don't have to waste computation power until we actually need the nodes. When we do so we can just use vanilla JS and map the lenses to their .view():

const johnsFriends = friends.lens('person1').list('friend').map(friendLens => friendLens.view());
console.log(johnsFriends);

// prints:
// [ { id: 'person2', name: 'Paul' },
//   { id: 'person3', name: 'George' },
//   { id: 'person4', name: 'Ringo' } ]

So by combining .list(type) and .view() you can navigate through the whole graph coming from one entry node. Because when you look at the .map() function you could again perform a .list(type) on each lens of the list and select nodes of further relation types.

All of this empowers you now to model one-to-many and many-to-many relationships between nodes. But what about one-to-one relationships?

lens(id).first(type)

Well got treats one-to-one relations as one-to-many (1-n) relations with n being 1. So in general there is the possibility of a node being connected to two other ones via the edges { from: 'human1', fromType: 'son', to: 'human2', toType: 'father'} and { from: 'human1', fromType: 'son', to: 'human3', toType: 'father'} which means that human1 would have two fathers which might be semantically wrong. got gives you no constraints on how you structure your graph. If it is wrong to have two fathers in the context of your application it is your responsibility to take care of it. But got gives you the sugar function .first(type) for selecting exactly one node of a certain relation type:

const { got } = require('gotjs');

const humans = got({
    nodes: {
        human1: { id: 'human1', name: 'Anakin' },
        human2: { id: 'human2', name: 'Luke' }
    },
    edges: [{
        from: 'human1',
        fromType: 'father',
        to: 'human2',
        toType: 'son'
    }]
});

const lukesFather = humans.lens('human2').first('father').view();
console.log(lukesFather);

// prints: { id: 'human1', name: 'Anakin' }

lens(id).prop(name)

Last but not least you should be able to access a property of a node. Of course you could do so by just viewing the node and calling the property with vanilla JS: .lens('person1').view().name. But got gives you the opportunity to access your nodes properties in a functional manner.

const { got } = require('gotjs');

const humans = got({
    nodes: {
        human1: { id: 'human1', name: 'Paul' },
        human2: { id: 'human2' }
    }
});

const name1 = humans.lens('human1').prop('name').get();
console.log(name1);
// prints: Paul

const name2 = humans.lens('human2').prop('name').get();
console.log(name2);
// prints: undefined

Of course in this case there is no difference to the .lens('person1').view().name approach, but when you try it yourself you notice that .prop() returns an Option of the value which you can use to pass the problem of an undefined property down the river until it is appropriate to decide what to do with undefined:

function writeGreeting(lens) {
    // .get() takes a default value as argument
    return lens.prop('name').map(name => 'Greetings to you, ' + name + '! 🧐').get('There is no name 😡');
}

console.log(writeGreeting(humans.lens('human1')));
// prints: Greetings to you, Paul! 🧐
console.log(writeGreeting(humans.lens('human2')));
// prints: There is no name 😡

Summary

Coming soon.

But What's Different?

Coming soon.