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

@ducharmemp/mobstr

v1.0.0-alpha.3

Published

Object relational mapper for MobX stores

Downloads

4

Readme

MobStr

CircleCI CodeCov npm version

MobStr is a project designed to provide an ORM-like interface to a MobX store. The goal of this project is to achieve a low-overhead, normalized approach to modeling to allow developers to focus on domain modeling without having to deal with manual book-keeping of object relationships. It also provides an opt-in approach to constraint checking, which can lead to better performance since we simply don't check every field if it's not marked with a check constraint.

Breaking Change for v1.0

Microbundle/rollup doesn't play well with named and default exports mixing. Therefore, all exports will now be explicitly named. The following code:

import initialize from 'mobstr'

Will need to be rewritten to:

import { initializeStore } from 'mobstr'

No other API changes are planned.

Benefits

  • Simple and direct query API to find/delete entries from the data store
  • Declarative API for describing object models in a manner similar to other data stores
  • Relationship cascades and auto-cleanup for removed objects
  • Separation of concerns for managing object models and RESTful/GraphQL API queries (no sagas/thunks)
  • Update objects directly without the need for complex copy operations
  • Opt-in semantics for constraints and checks on columns for debugging purposes and sanity checks.

Technical Details

Additionally, it's actually fairly easy to make certain guarantees that MobX provides invalid by complete accident, especially when copying string keys from observable objects into another observable. The answer is to leverage computed or autorun or other reaction-based functions, but this library should abstract over those to the point where the user doesn't need to necessarily worry about committing error-prone code in these specialized cases.

There do exist other solutions in the MobX examples and they are perfectly valid, but they require passing around parent contexts and there isn't an out of the box solution for saying "I have all of these models that I know are related to parents, but I just want these without looping through all of the parents". Consider this example store code loosely lifted from the MobX documentation:

class ToDo {
    constructor(store) {
        this.store = store;
    }
}

class Parent {
    @observable todos = []
    
    makeTodo() {
      this.todos.push(new ToDo(this));
    }
}

Full and complete sample here: https://mobx.js.org/best/store.html

This requires only a simple flatmap to achieve the desired output of a list of all ToDos, but more complicated relationships would easily become more cumbersome. For example, take the following code snippet:

class Step {}

class ToDo {
    @observable steps = [];
    
    makeStep() {
        this.steps.push(new Step(this))
    }

    constructor(store) {
        this.store = store;
    }
}

class Parent {
    @observable todos = []
    
    makeTodo() {
      this.todos.push(new ToDo(this));
    }
}

The overall approach is still the same (flatMap with a greater depth to get all Steps from all ToDos), but it would be nice to simply query for all of the steps that currently exist in isolation, or all ofthe ToDos that currently exist without having to traverse the parent contexts.

With this project, I hope to separate the concerns of managing a centralized store with an accessible syntax for describing model relationships and model structure. Eventually I also hope to integrate nice-to-have features, such as index only lookups, complex primary key structures, and relationship cascade options.

All told, this project is about 200 lines of actual code (so far!), with most of the actual code lying in the decorators to set up meta attributes and maintain book-keeping, so it should achieve a very similar result to mobx-state-tree while cutting down on the complexity. LOC isn't a great metric for complexity or scope but it's what I have on hand.

As of now, the form that the __meta__ attribute takes is this:

__meta__: {
    key: IObservableValue<string | symbol | number | null>;

    collectionName: string | symbol | number;
    relationships: Record<
      string | symbol,
      {
        type: any;
        keys: IObservableArray<string>;
        options: Record<string, any>;
      }
    >;
    indicies: IObservableArray<string | symbol | number>;
  };

Example of "proper" typing of class attributes:

class Bar {
    @primaryKey
    id: string = uuid();
}

class Foo {
    @primaryKey
    id: string = uuid();
    
    @relationship(type => Bar)
    friends: Bar[] = [];
}

const f = new Foo();
f.friends[0].id // This properly gives us type hints because we've typed it as a Bar[]. We could have also typed it as an IObservableArray

At this time, the recommended way to use POJOs in this library is similar to this example code:

class Foo {
    @primaryKey
    id = uuid();
    
    @observable
    someProperty = []
}

// returnValue = { status: 200, data: {id: '1234', someProperty: [1, 2, 3, 4] }}
function apiCallResult(returnValue) {
    // Validate
    ...
    // Dump the result into a new instance of the model
    const f = Object.assign(new Foo(), returnValue.data);
    add(f);
    return f;
}
class Foo {
  @primaryKey
  id: string = uuid();

  @relationship(store, () => Foo, { cascade: true })
  leaves: Foo[] = [];
}
const foo = new Foo();
const leaves = [new Foo(), new Foo()];
const otherLeaves = [new Foo(), new Foo()];
addOne(store, foo);
foo.leaves.push(...leaves);
leaves[0].leaves.push(...otherLeaves);
findAll(Foo).length === 5;
removeOne(foo);
findAll(Foo).length === 0;

However, this does still have the same limitations as POJOs currently do, so you can't directly shove a JSON structure into the store, there has to be a preprocessing step. However, a nice side effect of this is the ability to gather all Foo objects in a single query without walking the entirety of the tree.

At this time, no. It has a lot to do with when javascript class definitions are evaluated. For an example of what I'm talking about, please reference the below code:

class Bar {
  @primaryKey;
  id = uuid()
  
  @relationship(() => Foo)
  foos = [];
}

class Foo {
  @primaryKey
  id = uuid();
  
  @relationship(() => Bar)
  bars = []
}

At class definition time, "Foo" as a type is undefined, so the overall code will fail. I hope to eventually allow for these kinds of structures by using some form of lazy evalutaion on relationship definitions, similar to the method employed by SQLAlchemy.

No, since the decorator specification doens't allow for usage of decorators within anonymous classes, there's not much that MobStr can do at this time. I hope that in the future we could allow for something like this, since it could open up doors for dynamic model creation, although I'm not sure if that's a great idea or a terrible idea.

Examples

You can find some comprehensive toy examples in tests/integration.test.ts. Below is an example of real-world(ish) example using a fetch to get a company name and the a list of employees from that company.

import { observable, computed } from 'mobx';
import { initializeStore } from 'mobstr';

const {
    relationship,
    primaryKey,
    addAll,
    findOne,
    removeOne,
    truncateCollection,
    notNull,
} = initializeStore();

class Employee {
    @primaryKey
    id = uuid()
}

class Company {
    @primaryKey
    id = uuid()
    
    @notNull
    name;
    
    @observable
    affiliates = []
    
    @relationship(type => Employee)
    employees = [];
    
    @computed
    get employeeIds() { return this.employees.map(employee => employee.id); }
}

async function getAllCompanies(companyIds) {
    const companyData = await Promise.all(
      companyIds.map(companyId => fetch(`/url/for/company/${companyId}`)
    );
    companyData.forEach(company => {
      // Get all of the employee objects from the DB
      const employees = await Promise.all(
        company.employees.map(employee => fetch(`/url/for/company/employee/${employee}`))
      );
      
      // Note that this would overwrite any existing employees with the same ID in the data store, so make sure your IDs are unique!
      company.employees = employees.map(employee => Object.assign(new Employee(), employee))      
    });
    
    return companyData.map(company => Object.assign(new Company(), company));
}

// Top level await for illustrative purposes only
addAll(await getAllCompanies([1, 2, 3, 4))
findOne(Company, 1).employees.push(new Employee());

...
// Maybe we want to show a table of the company in one column with an employee in the other
join(Company, Employees).map((company, employee) => [company.id, employee.id])

...
function destroyCompany(companyId) {
    findOne(Company, companyId).employees = [];
    // If we had cascade: true in our relationship options, we could also delete the company from the store like so:
    // removeOne(findOne(Company, companyId));
}

// Example of a react component to display all companies and with a button to delete all employees for a given company
function ShowAllCompanies(props) {
    const companies = findAll(Company);
    return (
        <div>
            {
                companies.map(company => (
                    <div>
                        <span>{company.name}</span>
                        <button onClick={destroyCompany.bind(null, company.id)}>Destroy {company.name}?</button>
                    </div>
                ))
            }
        </div>
    );
}

Getting Started

This does require decorator support for now, so follow the instructions for enabling babel decorator support here: https://babeljs.io/docs/en/babel-plugin-proposal-decorators

If using TypeScript, enable the "experimentalDecorators" flag in tsconfig.json, instructions located here: https://www.typescriptlang.org/docs/handbook/decorators.html

If using Create-React-App in conjunction with this project and don't wish to eject, please use react-app-rewired to override the babel settings, located here: https://github.com/timarney/react-app-rewired

Running the tests

This project uses mocha/chai for testing purposes. To invoke, use npm test to run the test suite.

Built With

Versioning

We use SemVer for versioning. For the versions available, see the tags on this repository.

Authors

License

This project is licensed under the MIT License - see the LICENSE.md file for details