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

@arnesfield/unnest

v0.0.2

Published

Flatten nested objects to table rows.

Downloads

4

Readme

unnest

Flatten nested objects to table rows.

const { unnest } = require('@arnesfield/unnest');
const table = unnest(items).by(property);
const rows = table.rows();
const data = table.data();

Using TypeScript:

const table: Table<Schema> = unnest(items).by<Schema>(property);

Tip: Setting the Schema generic type should improve typings for Rows, Cells, and RowData.

Installation

npm install @arnesfield/unnest

Use the module:

// ES6
import unnest from '@arnesfield/unnest';

// CommonJS
const { unnest } = require('@arnesfield/unnest');

Use the UMD build:

<script src="https://unpkg.com/@arnesfield/unnest/lib/index.umd.js"></script>
const table = window.unnest(data).by(property);

Usage

Here is a basic example of unnesting a nested object:

const user = {
  email: '[email protected]',
  animals: [
    { type: 'cat', food: ['fish', 'meat'] },
    { type: 'frog', food: ['insects'] }
  ]
};

Use unnest to flatten the object:

const table = unnest(user).by({
  animals: {
    food: true
  }
});

Tip: Notice that the structure of the property value is similar to the nested object.

The table contains the Rows or RowData of the unnested object:

// get rows
const rows = table.rows();

// get data
const data = table.data();

Output of table.data():

Note: Most of the actual output structure is omitted for brevity.

[
  { root: /* user */, animals: /* cat */,  food: /* fish */    },
  {                                        food: /* meat */    },
  {                   animals: /* frog */, food: /* insects */ },
]

Using a table, the result would look something like this:

| root | animals | food | | ---- | ------- | ------- | | user | cat | fish | | | | meat | | | frog | insects |

If you're using TypeScript, the Schema type (similar to RowData) would look something like this:

interface Schema {
  root: User;
  animals: Animal;
  food: Food;
}

const table = unnest(user).by<Schema>(property);

unnest function and Property

The unnest function takes in the data (array or object) and calling .by(property) returns a table:

const table = unnest(data).by(property);

The property value structure is based on the data passed to the unnest function.

type PropertyValue = string | boolean | Property;

interface Property {
  // name of the property, defaults to the object property key or `root`
  name?: string;

  // other properties based on the data
  [property]: PropertyValue;
}

Consider the following interface:

interface User {
  email: string;
  aliases: string[];
  animals: {
    type: string;
    food: {
      kind: string;
      value: string[];
    }[];
  }[];
  groups?: {
    title: string;
    members: string[];
  }[];
}

The property value type may look like the following depending on how you want to unnest the object:

{
  // name: string,
  email: PropertyValue,
  aliases: PropertyValue,
  animals: {
    // name: string,
    type: PropertyValue,
    food: {
      // name: string,
      kind: PropertyValue,
      value: PropertyValue
    }
  },
  groups: {
    // name: string,
    title: PropertyValue,
    members: PropertyValue
  }
}

Each specified property will be included in the Row and RowData object.

Custom Column Name (property.name)

By default, the object property keys are used as the default column name (root is the default for the main object) similar to our example output a while back:

| root | animals | food | | ---- | ------- | ------- | | user | cat | fish | | | | meat | | | frog | insects |

Notice that the column names are root, animals, and food.

You can configure the column names by using the name property, or pass it as the property value:

const table = unnest(user).by({
  // root -> owner
  name: 'owner',
  animals: {
    // animals -> pet
    name: 'pet',
    // food -> treat
    food: 'treat' // can also be `food: { name: 'treat' }`
  }
});

Output of table.data() using a table:

| owner | pet | treat | | ----- | ---- | ------- | | user | cat | fish | | | | meat | | | frog | insects |

Notice that the columns are using the custom names.

Since the column names have changed, make sure the Schema type gets updated accordingly:

interface Schema {
  // root -> owner
  owner: User;
  // animals -> pet
  pet: Animal;
  // food -> treat
  treat: Food;
}

Row, Cell, and RowData

Before jumping in to the Table object, we'll need to know what are Rows, Cells, and RowData.

interface Row {
  group: string | number;
  cells: {
    [property]: Cell;
  };
}

interface Cell {
  data: /* cell data type */;
  group: string | number;
  span?: number;
}

type RowData<Schema> = Partial<Schema>;

What do these mean?

  • Row - contains the Cells.

  • Cell - contains the data.

  • RowData - the Schema but with partial values.

  • span - pertains to the rowspan of a Cell. It is set only for Cells that span across Rows.

  • group - contains a unique value which determines if Rows or Cells are related (or are in a group).

    By default, the group value uses the index of the array of data passed to unnest (if it's an object, the value is 0).

    You can set your own group value through the unnest function:

    unnest(users, (user, index, array) => user.email).by(property);

    Tip: The user.email is used as the group value.

Table

Using unnest(data).by(property) gives you a Table object.

The Table object contains the Rows and RowData that have been unnested, as well as other useful methods.

  • Get the rows.

    const rows = table.rows();
    
    // filter by group
    const rows = table.rows(group);
  • Get the row data.

    const data = table.data();
  • Transform Rows to RowData.

    const data = table.data(...rows);
  • Get the root rows (the main object/s or the first rows per group).

    const rows = table.roots();
  • Get all the cells in the column (property).

    const cells = table.column('treat');
    
    // filter by group
    const cells = table.column('treat', group);
    
    // set `includeEmpty` to `true` to include `undefined` cells
    const cells = table.column('treat', group, true);

    Tip: See treat property from the previous example.

  • Get the cell info (current, previous, and next cells) at row index if any.

    const rowIndex = 1;
    const info = table.cell('treat', rowIndex);

    Output of info:

    {
      current: /* Cell */ { data: 'meat', group: 0 },
      previous: /* Cell */ { data: 'fish', group: 0 },
      next: /* Cell */ { data: 'insects', group: 0 }
    }
  • table.filter(callback)

    Similar to array.filter(callback), but table.filter(callback) will return a new Table object with the filtered rows.

    The return value of the filter callback is an object similar to the Schema type.

    const filteredTable = table.filter((row, index, array) => {
      return {
        owner: /* true, false, undefined */ true,
        pet: /* true, false, undefined */ true,
        treat: /* true, false, undefined */ true
      };
    });
    const filteredRows = filteredTable.rows();
  • table.sort(compareFn)

    Similar to array.sort(compareFn), but only the root rows are used as the arguments for the compareFn.

    The return value of table.sort(compareFn) is also a new Table object similar to table.filter().

    const sortedTable = table.sort((rootRowA, rootRowB) => {
      return /* number */ 0;
    });
    const sortedRows = sortedTable.rows();

    By using the root rows as the arguments to compare, the other rows of the same group do not get sorted. Only the entire group is sorted against other groups.

    e.g. After sorting, the rows with group index 1 precede the rows with group index 0.

    Tip: The methods table.filter() and table.sort() return a new Table object to allow the usage of the Table methods on the new filtered/sorted rows instead.

  • Update Cell span values.

    table.updateSpans();

    Note that this will change the rows array, row, and cell references.

Merging Columns

There may be cases where the nested object would require its properties to be in one column.

This is already handled by unnest by placing the incoming cells last.

Consider this nested object:

const user = {
  email: '[email protected]',
  animals: [
    { type: 'cat', food: ['fish', 'meat'] },
    { type: 'frog', food: ['insects'] }
  ],
  food: ['chicken', 'beef']
};

Notice that there is food property for animals and the user object itself. Let's try to unnest this object:

const table = unnest(user).by({
  animals: {
    food: true
  },
  food: true
});

Output of table.data() using a table:

| root | animals | food | | ---- | ------- | ------- | | user | cat | fish | | | | meat | | | frog | insects | | | | chicken | | | | beef |

The user.food values (chicken and beef) come after the previous rows.

This merge feature should work in most cases as long as the property values are arranged in a way that works for you.

Tip: You can use a different column name for duplicating property names so they show up in a different column.

Special Cases

If the resulting output does not satisfy your needs, then you are free to directly mutate the rows array of table.

const rows = table.rows();

// mutate `rows` array directly, some examples:
rows.pop();
rows.push(row);
rows.sort(sortFn);
rows.splice(spliceFn);

// update cell span values
table.updateSpans();

Note that rows (also rows and cells) will have a different reference after calling table.updateSpans().

rows === table.rows(); // false

This method will allow you to merge 2 or more table.rows(). Directly updating rows means you would have to take note of the group value uniqueness.

render function

render(rows, getLabelFn);
render(rows, columns, getLabelFn);

A render function is included which accepts rows and returns a Markdown table string.

const { unnest, render } = require('@arnesfield/unnest');
// ...
const tableStr = render(table.rows(), row => {
  // convert to RowData so it's easier to work with
  const [data] = table.data(row);
  // labels per column, defaults to empty string
  return {
    owner: data.owner?.email,
    pet: data.pet?.type,
    treat: data.treat
  };
});
console.log(tableStr);

Output:

| owner            | pet  | treat   |
| ---------------- | ---- | ------- |
| [email protected] | cat  | fish    |
|                  |      | meat    |
|                  | frog | insects |

You can also pass in default columns to use. With this, you can reorder the columns to display:

const tableStr = render(table.rows(), ['treat', 'owner', 'pet'], row => {
  const [data] = table.data(row);
  return {
    owner: data.owner?.email,
    pet: data.pet?.type,
    treat: data.treat
  };
});
console.log(tableStr);

Output:

| treat   | owner            | pet  |
| ------- | ---------------- | ---- |
| fish    | [email protected] | cat  |
| meat    |                  |      |
| insects |                  | frog |

License

Licensed under the MIT License.