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 🙏

© 2026 – Pkg Stats / Ryan Hefner

keynest

v1.1.1

Published

Transform flat SQL join rows into nested objects and arrays using column-name conventions. Supports associations (single underscore), collections (double underscore), JSON Schema, and custom ID fields.

Readme

keynest

npm version Node.js License: MIT Tests

A JavaScript library that maps flat SQL join results into nested objects and arrays. Ideal for converting the denormalized rows returned by relational database queries into rich, hierarchical structures ready to be consumed by your API or application.

Table of contents


Installation

npm install keynest

Requires Node.js >= 14.


Quick start

const keynest = require('keynest');

// Static helper — maps an array of flat rows to a list of nested objects
const rows = [
  { id: 1, name: 'Alice', orders__id: 10, orders__total: 99.9 },
  { id: 1, name: 'Alice', orders__id: 11, orders__total: 49.5 },
  { id: 2, name: 'Bob',   orders__id: 12, orders__total: 19.0 },
];

const result = keynest.mapDynamic(rows);
// [
//   { id: 1, name: 'Alice', orders: [{ id: 10, total: 99.9 }, { id: 11, total: 49.5 }] },
//   { id: 2, name: 'Bob',   orders: [{ id: 12, total: 19.0 }] }
// ]

Naming conventions

keynest uses column name prefixes to determine how each field should be nested. The convention must be applied to the SQL column aliases in your query.

| Pattern | Result | Example column | Mapped to | | --- | --- | --- | --- | | field | Root-level property | name | obj.name | | assoc_field | Nested object (single _) | address_city | obj.address.city | | collection__field | Array item property (double __) | tags__name | obj.tags[n].name | | collection__ | Array of native values (empty suffix) | roles__ | obj.roles[n] |

The separator for collections is __ (two underscores). The separator for associations is _ (one underscore). When both separators appear in a key, the leftmost one wins. A column ending in __ (empty suffix after the double underscore) produces an array of primitive values instead of an array of objects.


Escaping the convention

When your data already contains underscores for other reasons (e.g. updated_at, created_at, snake_case_columns) you have two ways to tell keynest to leave those fields alone.

Option A — literalFields (field allow-list)

Explicitly mark certain fields as scalars. keynest will never apply the _ / __ convention to them.

const kn = new keynest();
kn.literalFields(['updated_at', 'created_at']);
// or pass a RegExp to match by pattern:
kn.literalFields(/_at$/);
// or a mixed array:
kn.literalFields(['updated_at', /_id$/]);

Static API:

keynest.mapDynamic(rows, schema, ids, { literalFields: ['updated_at', 'created_at'] });

Option B — separators (custom separator characters)

Change the characters that trigger nesting. This is useful when you can control the SQL aliases and prefer to keep underscores in plain field names.

const kn = new keynest();
kn.separators({ association: '$', collection: '$$' });
// Now:
//   currency$code  → { currency: { code: '...' } }
//   tags$$name     → { tags: [{ name: '...' }] }
//   updated_at     → { updated_at: '...' }  ✓  (plain, no $)

Static API:

keynest.mapDynamic(rows, schema, ids, { separators: { association: '$', collection: '$$' } });

Both options can be combined.


API reference

Static methods

keynest.mapDynamic(rows, schema?, ids?, options?)

Maps an array of flat rows to a list of nested objects.

| Parameter | Type | Default | Description | | --- | --- | --- | --- | | rows | Array | — | Flat rows from a SQL query | | schema | Object | undefined | Optional JSON Schema to define the output shape | | ids | string[] | ['id'] | Field names used as unique identifiers | | options | Object | {} | See below |

options

| Key | Type | Description | | --- | --- | --- | | literalFields | string \| RegExp \| Array<string\|RegExp> | Fields that bypass the naming convention | | separators | { association?: string, collection?: string } | Override the default _ / __ separators |

Returns Array.

keynest.mapDynamicToOne(rows, schema?, ids?, options?)

Maps an array of flat rows to a single nested object.

Same parameters as mapDynamic. Returns Object | undefined.


Instance methods

Use the instance API when you need to reuse the same configuration across multiple calls.

new keynest()

Creates a new mapper instance.

instance.toList(rows, schema?, objectsIds?)

Equivalent to mapDynamic but on an instance.

instance.toSingle(rows, schema?, objectsIds?)

Equivalent to mapDynamicToOne but on an instance.

instance.uniqueConventions(ids)

Overrides the default ID field (id) used to identify and deduplicate objects.

const kn = new keynest();
kn.uniqueConventions(['uuid', 'id']);

instance.literalFields(fields)

Mark fields that should bypass the naming convention and always remain as flat scalar properties.

| Parameter | Type | Description | | --- | --- | --- | | fields | string \| RegExp \| Array<string\|RegExp> | Field name(s) or pattern(s) to treat as literals |

kn.literalFields(['updated_at', 'created_at']);  // exact names
kn.literalFields(/_at$/);                         // RegExp
kn.literalFields(['updated_at', /_id$/]);         // mixed array

instance.separators(seps)

Override the characters used to detect associations (_) and collections (__).

| Parameter | Type | Description | | --- | --- | --- | | seps | { association?: string, collection?: string } | New separator characters |

kn.separators({ association: '$', collection: '$$' });
// currency$code  → { currency: { code: '...' } }
// tags$$name     → { tags: [{ name: '...' }] }
// updated_at     → stays as-is (no '$' in the name)

Examples

Simple flat rows

const rows = [
  { id: 1, name: 'Boston Celtics' },
  { id: 2, name: 'Los Angeles Lakers' },
];

keynest.mapDynamic(rows);
// [{ id: 1, name: 'Boston Celtics' }, { id: 2, name: 'Los Angeles Lakers' }]

Nested object (association)

Single underscore (_) maps a group of columns into a nested object.

// SQL: SELECT id, name, currency_code, currency_name FROM ...
const rows = [
  { id: '734a0722', name: 'Pangea Fund', currency_code: 'EUR', currency_name: 'Euro' }
];

keynest.mapDynamic(rows);
// [{
//   id: '734a0722',
//   name: 'Pangea Fund',
//   currency: { code: 'EUR', name: 'Euro' }
// }]

Array of objects (collection)

Double underscore (__) maps repeated rows into an array of nested objects.

// SQL: SELECT u.id, u.alias, a.scope AS authorizations__scope FROM users u
//       JOIN authorizations a ON a.user_id = u.id
const rows = [
  { id: 'U1', alias: 'alice', authorizations__scope: 'profile' },
  { id: 'U1', alias: 'alice', authorizations__scope: 'accounts' },
  { id: 'U2', alias: 'bob',   authorizations__scope: 'profile' },
];

keynest.mapDynamic(rows);
// [
//   { id: 'U1', alias: 'alice', authorizations: [{ scope: 'profile' }, { scope: 'accounts' }] },
//   { id: 'U2', alias: 'bob',   authorizations: [{ scope: 'profile' }] }
// ]

Mixed nesting

Associations and collections can be combined at any depth.

// Each fund row has scalar fields, a nested `currency` object and a nested `fees` array
const rows = [
  {
    id: '734a0722',
    name: 'Pangea Fund',
    currency_code: 'EUR',
    currency_name: 'Euro',
    fees__name: 'Redemption Fee',
    fees__value: 0,
  },
  {
    id: '734a0722',
    name: 'Pangea Fund',
    currency_code: 'EUR',
    currency_name: 'Euro',
    fees__name: 'Total Expense Ratio',
    fees__value: 1.5,
  },
];

keynest.mapDynamic(rows);
// [{
//   id: '734a0722',
//   name: 'Pangea Fund',
//   currency: { code: 'EUR', name: 'Euro' },
//   fees: [
//     { name: 'Redemption Fee', value: 0 },
//     { name: 'Total Expense Ratio', value: 1.5 }
//   ]
// }]

Arrays of native values

When a column name ends with __ and has no field name after it, the raw value becomes the array element directly — useful for arrays of strings, numbers, booleans, etc.

// SQL: SELECT u.id, r.name AS roles__ FROM users u JOIN user_roles r ON r.user_id = u.id
const rows = [
  { id: 'U1', roles__: 'admin' },
  { id: 'U1', roles__: 'editor' },
  { id: 'U2', roles__: 'viewer' },
];

keynest.mapDynamic(rows);
// [
//   { id: 'U1', roles: ['admin', 'editor'] },
//   { id: 'U2', roles: ['viewer'] }
// ]

This also works nested inside a collection:

// organizations__roles__ → obj.organizations[n].roles = ['adviser', ...]
const rows = [
  { id: 'U1', organizations__id: 'ORG1', organizations__roles__: 'adviser' },
  { id: 'U1', organizations__id: 'ORG1', organizations__roles__: 'manager' },
];

keynest.mapDynamic(rows);
// [{ id: 'U1', organizations: [{ id: 'ORG1', roles: ['adviser', 'manager'] }] }]

Mapping with a JSON Schema

Pass a JSON Schema to pre-initialize the output shape. This is useful to guarantee that optional arrays or objects always appear in the result even when no rows match them.

const schema = {
  properties: {
    code:         { type: 'number' },
    internalCode: { type: 'string' },
    message:      { type: 'string' },   // will be null if missing in rows
  }
};

const kn = new keynest();
kn.toSingle([{ code: 200, internalCode: '-3000' }], schema);
// { code: 200, internalCode: '-3000', message: null }

JSON Schema allOf is automatically resolved via json-schema-merge-allof.


Custom ID fields

By default, keynest uses id to identify and deduplicate objects. Override this with uniqueConventions or the ids parameter.

// Instance API
const kn = new keynest();
kn.uniqueConventions(['uuid']);
kn.toList(rows);

// Static API
keynest.mapDynamic(rows, undefined, ['uuid']);

Literal fields

Prevent specific snake_case fields from being treated as associations.

const rows = [
  { id: 1, name: 'Alice', updated_at: '2024-01-01', address_city: 'NY' }
];

// Instance API
const kn = new keynest();
kn.literalFields(['updated_at']);
kn.toList(rows);
// [{ id: 1, name: 'Alice', updated_at: '2024-01-01', address: { city: 'NY' } }]

// Static API — pass a 4th options argument
keynest.mapDynamic(rows, undefined, ['id'], { literalFields: ['updated_at'] });

// RegExp — mark all *_at and *_by fields as literals
kn.literalFields(/_(at|by)$/);

Custom separators

Switch to a different separator character so that underscores in column names are never interpreted as nesting markers.

// SQL: SELECT id, updated_at,
//             currency$code, currency$name,
//             tags$$name
// FROM ...
const rows = [
  { id: 1, updated_at: '2024-01-01', currency$code: 'EUR', currency$name: 'Euro', tags$$name: 'bonds' },
  { id: 1, updated_at: '2024-01-01', currency$code: 'EUR', currency$name: 'Euro', tags$$name: 'equity' },
];

const kn = new keynest();
kn.separators({ association: '$', collection: '$$' });
kn.toList(rows);
// [{
//   id: 1,
//   updated_at: '2024-01-01',       // ← plain, untouched
//   currency: { code: 'EUR', name: 'Euro' },
//   tags: [{ name: 'bonds' }, { name: 'equity' }]
// }]

// Static API
keynest.mapDynamic(rows, undefined, ['id'], { separators: { association: '$', collection: '$$' } });

Debug

keynest uses the debug package. Enable verbose output by setting the DEBUG environment variable:

DEBUG=keynest:map node your-app.js

Operations that exceed 300 ms are automatically flagged in the debug output.


Testing

npm test

The test suite uses Mocha with 22 test cases covering simple mappings, associations, collections, nested structures, schema-driven output, and edge cases. Coverage is collected via nyc in lcov format.

22 passing

License

MIT