@arnesfield/unnest
v0.0.2
Published
Flatten nested objects to table rows.
Downloads
4
Maintainers
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 forRow
s,Cell
s, andRowData
.
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 unnest
ing 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 Row
s or RowData
of the unnest
ed 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 Row
s, Cell
s, 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 theCell
s.Cell
- contains the data.RowData
- theSchema
but with partial values.span
- pertains to therowspan
of aCell
. It is set only forCell
s that span acrossRow
s.group
- contains a unique value which determines ifRow
s orCell
s are related (or are in agroup
).By default, the
group
value uses theindex
of the array ofdata
passed tounnest
(if it's an object, the value is0
).You can set your own
group
value through theunnest
function:unnest(users, (user, index, array) => user.email).by(property);
Tip: The
user.email
is used as thegroup
value.
Table
Using unnest(data).by(property)
gives you a Table
object.
The Table
object contains the Row
s and RowData
that have been unnest
ed, 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
Row
s 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
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)
, buttable.filter(callback)
will return a newTable
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 thecompareFn
.The return value of
table.sort(compareFn)
is also a newTable
object 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
group
index1
precede the rows withgroup
index0
.Tip: The methods
table.filter()
andtable.sort()
return a newTable
object to allow the usage of theTable
methods on the new filtered/sorted rows instead.Update
Cell
span values.table.updateSpans();
Note that this will change the
rows
array,row
, andcell
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 row
s and cell
s) 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.