@arnesfield/unnest v0.0.2
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
Schemageneric type should improve typings forRows,Cells, andRowData.
Installation
npm install @arnesfield/unnestUse 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: 'john.doe@foo.bar',
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
propertyvalue 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 theCells.Cell- contains the data.RowData- theSchemabut with partial values.span- pertains to therowspanof aCell. It is set only forCells that span acrossRows.group- contains a unique value which determines ifRows orCells are related (or are in agroup).By default, the
groupvalue uses theindexof the array ofdatapassed tounnest(if it's an object, the value is0).You can set your own
groupvalue through theunnestfunction:unnest(users, (user, index, array) => user.email).by(property);Tip: The
user.emailis used as thegroupvalue.
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 toRowData.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
treatproperty 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), buttable.filter(callback)will return a newTableobject with the filtered rows.The return value of the filter callback is an object similar to the
Schematype.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 thecompareFn.The return value of
table.sort(compareFn)is also a newTableobject similar totable.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
groupindex1precede the rows withgroupindex0.Tip: The methods
table.filter()andtable.sort()return a newTableobject to allow the usage of theTablemethods on the new filtered/sorted rows instead.Update
Cellspan values.table.updateSpans();Note that this will change the
rowsarray,row, andcellreferences.
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: 'john.doe@foo.bar',
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(); // falseThis 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 |
| ---------------- | ---- | ------- |
| john.doe@foo.bar | 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 | john.doe@foo.bar | cat |
| meat | | |
| insects | | frog |License
Licensed under the MIT License.