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

@artisnull/norm

v2.1.0

Published

Turn your messy data into normalized bliss, with lots of customization along the way

Downloads

7

Readme

Coverage Status

NORM

Turn your messy data into normalized bliss, with lots of customization along the way

Turns this

const data = {
  posts: [
    {
      id: '0000',
      description: 'A post about nothing',
      thumbnail: {
        url: 'pathToImage',
        id: '0003',
      }
    },
    {
      id: '0001',
      description: 'A post about something',
      thumbnail: {
        url: 'pathToImage',
        id: '0004',
      }
    },
    {
      id: '0002',
      description: 'A post about life',
      thumbnail: {
        url: 'pathToImage',
        id: '0005',
      }
    },
  ]
}

into

const result = {
  posts: {
    byId: {
      '0000': {
        id: '0000',
        description: 'A post about nothing',
        thumbnail: ['0003'],
      },
      '0001': {
        id: '0001',
        description: 'A post about something',
        thumbnail: ['0004'],
      },
      '0002': {
        id: '0002',
        description: 'A post about life',
        thumbnail: ['0005'],
      },
    },
    allIds: ['0000', '0001', '0002'],
  },
  thumbnail: {
    byId: {
      '0003': { url: 'pathToImage', id: '0003' },
      '0004': { url: 'pathToImage', id: '0004' },
      '0005': { url: 'pathToImage', id: '0005' },
    },
    allIds: ['0003', '0004', '0005'],
  },
}

And this is all the code you need to do it:

const norm = new Norm()
norm.addRoot('posts', {thumbnail: 'id'})

const result = norm.normalize(data.posts)

:tada: :tada: :tada:


Table of Contents


Usage

Basic Single Node

All you need to get started is:

const data = {
  "posts": [
    {
      "id": "0000",
      "description": "A post about nothing"
    },
    {
      "id": "0001",
      "description": "A post about something"
    },
    {
      "id": "0002",
      "description": "A post about life"
    }
  ]
}

A minimal setup:

import Norm from '@artisnull/norm'

const norm = new Norm()
norm.addRoot('posts')

const result = norm.normalize(data.posts)

Which gives you:

const result = {
  "posts": {
    "byId": {
      "000": {
        "id": "0000",
        "description": "A post about nothing"
      },
      "0001": {
        "id": "0001",
        "description": "A post about something"
      },
      "0002": {
        "id": "0002",
        "description": "A post about life"
      }
    },
    "allIds": ["0000", "0001", "0002"] 
  }
}

Basic Multi Node

What if our example had nested data that we also wanted to normalize?

Let's take this data:

const data = {
  posts: [
    {
      id: '0000',
      description: 'A post about nothing',
      thumbnail: {
        url: 'pathToImage',
        id: '0003',
      }
    },
    {
      id: '0001',
      description: 'A post about something',
      thumbnail: {
        url: 'pathToImage',
        id: '0004',
      }
    },
    {
      id: '0002',
      description: 'A post about life',
      thumbnail: {
        url: 'pathToImage',
        id: '0005',
      }
    },
  ]
}

And modify our single node example slightly, adding {thumbnail: 'id'} as the second argument:

const norm = new Norm()
norm.addRoot('posts', {thumbnail: 'id'})

const result = norm.normalize(data.posts)

Which results in:

result = {
  posts: {
    byId: {
      '0000': {
        id: '0000',
        description: 'A post about nothing',
        thumbnail: ['0003'],
      },
      '0001': {
        id: '0001',
        description: 'A post about something',
        thumbnail: ['0004'],
      },
      '0002': {
        id: '0002',
        description: 'A post about life',
        thumbnail: ['0005'],
      },
    },
    allIds: ['0000', '0001', '0002'],
  },
  thumbnail: {
    byId: {
      '0003': { url: 'pathToImage', id: '0003' },
      '0004': { url: 'pathToImage', id: '0004' },
      '0005': { url: 'pathToImage', id: '0005' },
    },
    allIds: ['0003', '0004', '0005'],
  },
}

Notice how the nested references are replaced with the id(s) of the data that used to be there


API

Norm({silent: boolean}) : norm

default silent = true

Constructor, configure silent mode on or off.

silent: false Displays warnings that are sometimes helpful for debugging

import Norm from '@artisnull/norm'
const norm = new Norm()

norm.addRoot(name: string, subNodes: object, options: Options) : {[subNodeName]: subNode, ...subNodes}

Defines the root node of your data, allowing you describe the associated subnodes, and customize how you want the normalization process to take place.

The root is always normalized by the key id. If you need another key used instead, see Customizing single node structure

Returns subNodes corresponding to the subNodes you passed in.

You call subNode.define on those subNodes to further configure the process if necessary

See SubNodes Definition for ways to define your subNodes

Usage:

const {thumbnail} = norm.addRoot('posts', {thumbnail: 'id'}})

subNode.define(subNodes: object, options: Options) : {[subNodeName]: node, ...subNodes}

Defines this node of your data, allowing you describe the associated subnodes, and customize how you want the normalization process to take place

Returns subNodes corresponding to the subNodes you passed in.

You call subNode.define on those subNodes to further configure the process if necessary

See SubNodes Definition for ways to define your subNodes

Usage:

const {thumbnail, users} = norm.addRoot('posts', {thumbnail: 'id', users: 'name'})
const {sources} = thumbnail.define({sources: 'resolution'}, {additionalIds: ['url']})

norm.normalize(data: (object | Array)) : object

Normalizes your array or object based on the nodes you've defined using norm.addRoot and subNode.define

Usage:

const result = norm.normalize(data)

SubNodes Definition: object

Define the subNodes you want to further normalize

Can take the following forms:

// The following two are equivalent
{[subNodeName]: 'keyToNormalizeBy'}
{[subNodeName]: slice => slice['keyToNormalizeBy']}

// Or if you don't want to add subnodes, but do want to add options
{[subNodeName]: {}, {...options}}
{[subNodeName]: undefined, {...options}}

For example {thumbnail: 'id'}, thumbnail is the subNode name and it will be normalized by the values in thumbnail.id and is equivalent to {thumbnail: slice => slice.id}


Options : object

Modify and customize how you want your data to normalize

const options = {
    additionalIds: ['name'],
    filter: slice => slice.isGood,
    omit: true,
    resolve: {
        subNode1: slice => 'slice.subNode2'
    },
    transform: slice => {
        slice.tranformed = true
        return slice
    }
}

An example using every option is included: Multi Branch with every option

options.additionalIds : Array<string>

Define additional ids to be included in allIds.

Useful for when data can't be identified by id alone

Usage:

// Given this data
const data = {
    node: [
        {
            id: 'id',
            type: 'node'
        }
    ]
}
// Given this node
node.define(undefined, {
    // Should be in the form
    additionalIds: ['type']
})

After normalizing:

result = {
    node: {
        byId:{...},
        allIds: [{'id': 'id', type: 'node'}]
    }
}

options.filter() : boolean

Filter data from being saved into the normalized data structure.

Useful for keeping data out of the result.

options.filter() => true saves data

options.filter() => false does not save data

Note: Called before options.transform()

Usage:

// Given this data
const data = [
    {
        id: 'id',
        type: null
    },
    {
        id: 'id2',
        type: 'node'
    },
]
// Given this node
norm.addRoot('node', {}, {
    // Should be in the form
    filter: slice => {
        // slice = corresponding {id, type} object
        return !!slice.type // true to save, false to skip
    }
})

After normalizing:

result = {
    node: {
        byId:{
            id2: {...}
        },
        allIds: ['id2']
    }
}

options.omit : boolean

default: false

Toggle saving this node into the normalized structure

Useful for keeping data out of the result, has no effect on subNodes

options.omit = true does not save data

options.omit = false saves data

Usage:

// Given this data
const data = {
    root: {
        node: [...]
    }
}
// Given this node
root.define({node: 'id'}, {
    // Should be in the form
    omit: true
})

After normalizing:

result = {
    node: {
        byId:{...},
        allIds: [...]
    }
}

Notice there is no root key in the result


options.resolve : object

Describe how to find a subnode using "object.dot" notation.

Useful for renaming a node, dynamically selecting a node, or cherry-picking a nested node

Usage:

// Given this data
const data = {
    node: [
        {
            id: 'id',
            bar: {
                id: 'bar-id'
            }
        }
    ]
}
// Given this node
node.define({foo: 'id'}, {
    // Should be in the form
    resolve: {
        foo: slice => {
            // slice = {id: 'id', bar: {id: 'bar-id'}}
            return 'slice.bar'
        }
    }
})

After normalizing:

result = {
    node: {
        byId:{
            id: {id: 'id', foo: ['bar-id']}
        },
        allIds: ['id']
    },
    foo: {
        byId:{
            id: {id: 'bar-id'}
        },
        allIds: ['bar-id']
    }
}

Notice that node.bar has been replaced with node.foo

The above is an example of using options.resolve to rename a node


options.transform() : object

Transform a node being saved into the normalized structure.

Note: Called after options.filter()

Useful for cleaning up or adding to a node

Usage:

// Given this data
const data = {
    node: [
        {
            id: 'id',
        }
    ]
}
// Given this node
node.define(undefined, {
    // Should be in the form
    transform: slice => {
        // slice = {id: 'id'}
        return {...slice, transformed: true}
    }
})

After normalizing:

result = {
    node: {
        byId:{
            id: {id: 'id', transformed: true}
        },
        allIds: ['id']
    }
}

Note: the id key cannot be modified in the options.transform() function


Advanced examples

Customizing Single Node Structures

Problem

Because the root node is always normalized by id, you may run into issues with data like this:

const data = [
    {name: 'Paul'},
    {name: 'Jill'},
    {name: 'Sam'},
]
norm.addRoot('users')
norm.normalize(data) // won't work :(

There's no id keys to normalize by!

Solution

Wrap the data with a root node that will be omitted

const data = [
    {name: 'Paul'},
    {name: 'Jill'},
    {name: 'Sam'},
]
const {users} = norm.addRoot('root', {users: 'name'}, {omit: true})
norm.normalize({users: data}) // WILL work :)

Now users will be normalized by name as desired.


Multi branch with every option

Let's take more complicated data, and use every available option

{
  return {
    posts: [
      {
        id: '0000',
        description: 'A post about nothing',
        thumbnail: {
          url: 'pathToImage',
          id: '0003',
        },
        user: {
          name: 'Albert',
          type: 'normal',
          thumbnail: {
            url: 'pathToImage',
            id: '0000',
          },
        },
      },
      {
        id: '0001',
        description: 'A post about something',
        thumbnail: {
          url: 'pathToImage',
          id: '0004',
        },
        user: {
          name: 'James',
          type: 'normal',
          thumbnail: {
            url: 'pathToImage',
            id: '0001',
          },
        },
      },
      {
        id: '0002',
        description: 'A post about life',
        thumbnail: {
          url: 'pathToImage',
          id: '0004',
        },
        user: {
          name: 'Samantha',
          type: 'normal',
          thumbnail: {
            url: 'pathToImage',
            id: '0002',
          },
        },
      },
    ],
    meta: {
      posts: {
        allUsers: [
          {
            name: 'Albert',
            type: 'normal',
            thumbnail: {
              url: 'pathToImage',
              id: '0000',
            },
          },
          {
            name: 'James',
            type: 'normal',
            thumbnail: {
              url: 'pathToImage',
              id: '0001',
            },
          },
          {
            name: 'Samantha',
            type: 'normal',
            thumbnail: {
              url: 'pathToImage',
              id: '0002',
            },
          },
        ]
      }
    }
  }
}

We want to have a lot of control over this one:

const norm = new Norm()
const { renamedPosts } = norm.addRoot(
    'root',
    { renamedPosts: 'id' },
    {
    resolve: {
        renamedPosts: slice => 'slice.posts', // renames posts => renamedPosts
    },
    omit: true, // don't save root node
    },
)

const { thumbnail } = renamedPosts.define(
    { thumbnail: 'id' },
    {
    resolve: {
        thumbnail: slice => 'slice.user.thumbnail', // subNode is nested in another object
    },
    filter: slice => slice.id === '0002', // only allow the node with id === '0002'
    additionalIds: ['description'], // add 'description' to allIds
    },
)

thumbnail.define({}, {
    transform: slice => { // delete the url from the thumbnail subNode
        const s = {...slice}
        delete s.url
        return s
    },
})
const result = norm.normalize(sampleData)

Which results in:

{
    "thumbnail": {
        "byId": {
            "0000": {
                "id": "0000"
            },
            "0001": {
                "id": "0001"
            },
            "0002": {
                "id": "0002"
            }
        },
        "allIds": ["0000", "0001", "0002"]
    },
    "renamedPosts": {
        "byId": {
            "0002": {
                "id": "0002",
                "description": "A post about life",
                "thumbnail": {
                    "url": "pathToImage",
                    "id": "0004"
                },
                "user": {
                    "name": "Samantha",
                    "type": "normal",
                    "thumbnail": ["0002"]
                }
            }
        },
        "allIds": [{"id": "0002", "description": "A post about life"}]
    }
}

Note that renamedPosts.thumbnail is not normalized, this is becuse we resolved the thumbnail subNode to be renamedPosts.user.thumbnail
Also Note that the meta slice isn't in the normalized structure. It was not referenced, so it was not included.