Skip to content

avoidwork/haro

Repository files navigation

Haro

npm version License: BSD-3 Build Status

A simple, fast, and flexible way to organize and search your data.


Need a simple way to keep track of information—like contacts, lists, or notes? Haro helps you organize, find, and update your data quickly, whether you’re using it in a website, an app, or just on your computer. It’s like having a super-organized digital assistant for your information.

Table of Contents

Key Features

  • Easy to use: Works out of the box, no complicated setup.
  • Very fast: Quickly finds and updates your information.
  • Keeps a history: Remembers changes, so you can see what something looked like before.
  • Flexible: Use it for any type of data—contacts, tasks, notes, and more.
  • Works anywhere: Use it in your website, app, or server.

How Does It Work?

Imagine you have a box of index cards, each with information about a person or thing. Haro helps you sort, search, and update those cards instantly. If you make a change, Haro remembers the old version too. You can ask Haro questions like “Who is named Jane?” or “Show me everyone under 30.”

Who Is This For?

  • Anyone who needs to keep track of information in an organized way.
  • People building websites or apps who want an easy way to manage data.
  • Developers looking for a fast, reliable data storage solution.

Installation

Install with npm:

npm install haro

Or with yarn:

yarn add haro

Usage

Haro is available as both an ES module and CommonJS module.

Import (ESM)

import { haro } from 'haro';

Require (CommonJS)

const { haro } = require('haro');

Creating a Store

Haro takes two optional arguments: an array of records to set asynchronously, and a configuration object.

const storeDefaults = haro();
const storeRecords = haro([
  { name: 'Alice', age: 30 },
  { name: 'Bob', age: 28 }
]);
const storeCustom = haro(null, { key: 'id' });

Examples

Example 1: Manage a Contact List

import { haro } from 'haro';

// Create a store with indexes for name and email
const contacts = haro(null, { index: ['name', 'email'] });

// Add realistic contacts
contacts.batch([
  { name: 'Alice Johnson', email: '[email protected]', company: 'Acme Corp', phone: '555-1234' },
  { name: 'Carlos Rivera', email: '[email protected]', company: 'Rivera Designs', phone: '555-5678' },
  { name: 'Priya Patel', email: '[email protected]', company: 'InnovateX', phone: '555-8765' }
], 'set');

// Find a contact by email
console.log(contacts.find({ email: '[email protected]' }));
// → [[$uuid, { name: 'Carlos Rivera', email: 'carlos.r@example.com', company: 'Rivera Designs', phone: '555-5678' }]]

// Search contacts by company
console.log(contacts.search(/^acme/i, 'company'));
// → [[$uuid, { name: 'Alice Johnson', email: 'alice.j@example.com', company: 'Acme Corp', phone: '555-1234' }]]

// Search contacts with phone numbers ending in '78'
console.log(contacts.search(phone => phone.endsWith('78'), 'phone'));
// → [[$uuid, { name: 'Carlos Rivera', ... }]]

Example 2: Track Project Tasks

import { haro } from 'haro';

// Create a store for project tasks, indexed by status and assignee
const tasks = haro(null, { index: ['status', 'assignee'] });

tasks.batch([
  { title: 'Design homepage', status: 'in progress', assignee: 'Alice', due: '2025-05-20' },
  { title: 'Fix login bug', status: 'open', assignee: 'Carlos', due: '2025-05-18' },
  { title: 'Deploy to production', status: 'done', assignee: 'Priya', due: '2025-05-15' }
], 'set');

// Find all open tasks
console.log(tasks.find({ status: 'open' }));
// → [[$uuid, { title: 'Fix login bug', status: 'open', assignee: 'Carlos', due: '2025-05-18' }]]

// Search tasks assigned to Alice
console.log(tasks.search('Alice', 'assignee'));
// → [[$uuid, { title: 'Design homepage', ... }]]

Example 3: Track Order Status Changes (Versioning)

import { haro } from 'haro';

// Enable versioning for order tracking
const orders = haro(null, { versioning: true });

// Add a new order and update its status
let rec = orders.set(null, { id: 1001, customer: 'Priya Patel', status: 'processing' });
rec = orders.set(rec[0], { id: 1001, customer: 'Priya Patel', status: 'shipped' });
rec = orders.set(rec[0], { id: 1001, customer: 'Priya Patel', status: 'delivered' });

// See all status changes for the order
orders.versions.get(rec[0]).forEach(([data]) => console.log(data));
// Output:
// { id: 1001, customer: 'Priya Patel', status: 'processing' }
// { id: 1001, customer: 'Priya Patel', status: 'shipped' }
// { id: 1001, customer: 'Priya Patel', status: 'delivered' }

// { note: 'Initial' }
// { note: 'Updated' }

These examples show how Haro can help you manage contacts, tasks, and keep a history of changes with just a few lines of code.

Configuration

beforeBatch

Function

Event listener for before a batch operation, receives type, data.

beforeClear

Function

Event listener for before clearing the data store.

beforeDelete

Function

Event listener for before a record is deleted, receives key, batch.

beforeSet

Function

Event listener for before a record is set, receives key, data.

index

Array

Array of values to index. Composite indexes are supported, by using the default delimiter (this.delimiter). Non-matches within composites result in blank values.

Example of fields/properties to index:

const store = haro(null, {index: ['field1', 'field2', 'field1|field2|field3']});

key

String

Optional Object key to utilize as Map key, defaults to a version 4 UUID if not specified, or found.

Example of specifying the primary key:

const store = haro(null, {key: 'field'});

logging

Boolean

Logs persistent storage messages to console, default is true.

onbatch

Function

Event listener for a batch operation, receives two arguments ['type', Array].

onclear

Function

Event listener for clearing the data store.

ondelete

Function

Event listener for when a record is deleted, receives the record key.

onoverride

Function

Event listener for when the data store changes entire data set, receives a String naming what changed (indexes or records).

onset

Function

Event listener for when a record is set, receives an Array.

versioning

Boolean

Enable/disable MVCC style versioning of records, default is false. Versions are stored in Sets for easy iteration.

Example of enabling versioning:

const store = haro(null, {versioning: true});

Properties

data

Map

Map of records, updated by del() & set().

indexes

Map

Map of indexes, which are Sets containing Map keys.

registry

Array

Array representing the order of this.data.

size

Number

Number of records in the DataStore.

versions

Map

Map of Sets of records, updated by set().

API

batch(array, type)

Array

The first argument must be an Array, and the second argument must be del or set.

const haro = require('haro'),
    store = haro(null, {key: 'id', index: ['name']}),
    nth = 100,
    data = [];

let i = -1;

while (++i < nth) {
  data.push({id: i, name: 'John Doe' + i});
}

// records is an Array of Arrays
const records = store.batch(data, 'set');

clear()

self

Removes all key/value pairs from the DataStore.

Example of clearing a DataStore:

const store = haro();

// Data is added

store.clear();

del(key)

Undefined

Deletes the record.

Example of deleting a record:

const store = haro(),
  rec = store.set(null, {abc: true});

store.del(rec[0]);
console.log(store.size); // 0

dump(type="records")

Array or Object

Returns the records or indexes of the DataStore as mutable Array or Object, for the intention of reuse/persistent storage without relying on an adapter which would break up the data set.

const store = haro();

// Data is loaded

const records = store.dump();
const indexes = store.dump('indexes');

// Save records & indexes

entries()

MapIterator

Returns a new Iterator object that contains an array of [key, value] for each element in the Map object in insertion order.

Example of deleting a record:

const store = haro();
let item, iterator;

// Data is added

iterator = store.entries();
item = iterator.next();

do {
  console.log(item.value);
  item = iterator.next();
} while (!item.done);

filter(callbackFn[, raw=false])

Array

Returns an Array of double Arrays with the shape [key, value] for records which returned true to callbackFn(value, key).

Example of filtering a DataStore:

const store = haro();

// Data is added

store.filter(function (value) {
  return value.something === true;
});

find(where[, raw=false])

Array

Returns an Array of double Arrays with found by indexed values matching the where.

Example of finding a record(s) with an identity match:

const store = haro(null, {index: ['field1']});

// Data is added

store.find({field1: 'some value'});

forEach(callbackFn[, thisArg])

Undefined

Calls callbackFn once for each key-value pair present in the Map object, in insertion order. If a thisArg parameter is provided to forEach, it will be used as the this value for each callback.

Example of deleting a record:

const store = haro();

store.set(null, {abc: true});
store.forEach(function (value, key) {
  console.log(key);
});

get(key[, raw=false])

Array

Gets the record as a double Array with the shape [key, value].

Example of getting a record with a known primary key value:

const store = haro();

// Data is added

store.get('keyValue');

has(key)

Boolean

Returns a Boolean indicating if the data store contains key.

Example of checking for a record with a known primary key value:

const store = haro();

// Data is added

store.has('keyValue'); // true or false

keys()

MapIterator

Returns a new Iterator object that contains the keys for each element in the Map object in insertion order.`

Example of getting an iterator, and logging the results:

const store = haro();
let item, iterator;

// Data is added

iterator = store.keys();
item = iterator.next();

do {
  console.log(item.value);
  item = iterator.next();
} while (!item.done);

limit(offset=0, max=0, raw=false)

Array

Returns an Array of double Arrays with the shape [key, value] for the corresponding range of records.

Example of paginating a data set:

const store = haro();

let ds1, ds2;

// Data is added

console.log(store.size);  // >10
ds1 = store.limit(0, 10);  // [0-9]
ds2 = store.limit(10, 10); // [10-19]

console.log(ds1.length === ds2.length); // true
console.log(JSON.stringify(ds1[0][1]) === JSON.stringify(ds2[0][1])); // false

map(callbackFn, raw=false)

Array

Returns an Array of the returns of callbackFn(value, key). If raw is true an Array is returned.

Example of mapping a DataStore:

const store = haro();

// Data is added

store.map(function (value) {
  return value.property;
});

override(data[, type="records", fn])

Boolean

This is meant to be used in a paired override of the indexes & records, such that you can avoid the Promise based code path of a batch() insert or load(). Accepts an optional third parameter to perform the transformation to simplify cross domain issues.

Example of overriding a DataStore:

const store = haro();

store.override({'field': {'value': ['pk']}}, "indexes");

reduce(accumulator, value[, key, ctx=this, raw=false])

Array

Runs an Array.reduce() inspired function against the data store (Map).

Example of filtering a DataStore:

const store = haro();

// Data is added

store.reduce(function (accumulator, value, key) {
  accumulator[key] = value;

  return accumulator;
}, {});

reindex([index])

Haro

Re-indexes the DataStore, to be called if changing the value of index.

Example of mapping a DataStore:

const store = haro();

// Data is added

// Creating a late index
store.reindex('field3');

// Recreating indexes, this should only happen if the store is out of sync caused by developer code.
store.reindex();

search(arg[, index=this.index, raw=false])

Array

Returns an Array of double Arrays with the shape [key, value] of records found matching arg. If arg is a Function (parameters are value & index) a match is made if the result is true, if arg is a RegExp the field value must .test() as true, else the value must be an identity match. The index parameter can be a String or Array of Strings; if not supplied it defaults to this.index.

Indexed Arrays which are tested with a RegExp will be treated as a comma delimited String, e.g. ['hockey', 'football'] becomes 'hockey, football' for the RegExp.

Example of searching with a predicate function:

const store = haro(null, {index: ['department', 'salary']}),
  employees = [
    { name: 'Alice Johnson', department: 'Engineering', salary: 120000 },
    { name: 'Carlos Rivera', department: 'Design', salary: 95000 },
    { name: 'Priya Patel', department: 'Engineering', salary: 130000 }
  ];

store.batch(employees, 'set');
// Find all employees in Engineering making over $125,000
console.log(store.search((salary, department) => department === 'Engineering' && salary > 125000, ['salary', 'department']));
// → [[$uuid, { name: 'Priya Patel', department: 'Engineering', salary: 130000 }]]

set(key, data, batch=false, override=false)

Object

Record in the DataStore. If key is false a version 4 UUID will be generated.

If override is true, the existing record will be replaced instead of amended.

Example of creating a record:

const store = haro(null, {key: 'id'}),
  record = store.set(null, {id: 1, name: 'John Doe'});

console.log(record); // [1, {id: 1, name: 'Jane Doe'}]

sort(callbackFn, [frozen = true])

Array

Returns an Array of the DataStore, sorted by callbackFn.

Example of sorting like an Array:

const store = haro(null, {index: ['name', 'age']}),
   data = [{name: 'John Doe', age: 30}, {name: 'Jane Doe', age: 28}];

store.batch(data, 'set')
console.log(store.sort((a, b) => a < b ? -1 : (a > b ? 1 : 0))); // [{name: 'Jane Doe', age: 28}, {name: 'John Doe', age: 30}]

sortBy(index[, raw=false])

Array

Returns an Array of double Arrays with the shape [key, value] of records sorted by an index.

Example of sorting by an index:

const store = haro(null, {index: ['priority', 'due']}),
  tickets = [
    { title: 'Fix bug #42', priority: 2, due: '2025-05-18' },
    { title: 'Release v2.0', priority: 1, due: '2025-05-20' },
    { title: 'Update docs', priority: 3, due: '2025-05-22' }
  ];

store.batch(tickets, 'set');
console.log(store.sortBy('priority'));
// → Sorted by priority ascending

toArray([frozen=true])

Array

Returns an Array of the DataStore.

Example of casting to an Array:

const store = haro(),
  notes = [
    { title: 'Call Alice', content: 'Discuss Q2 roadmap.' },
    { title: 'Email Carlos', content: 'Send project update.' }
  ];

store.batch(notes, 'set');
console.log(store.toArray());
// → [
//   { title: 'Call Alice', content: 'Discuss Q2 roadmap.' },
//   { title: 'Email Carlos', content: 'Send project update.' }
// ]

values()

MapIterator

Returns a new Iterator object that contains the values for each element in the Map object in insertion order.

Example of iterating the values:

const store = haro(),
   data = [{name: 'John Doe', age: 30}, {name: 'Jane Doe', age: 28}];

store.batch(data, 'set')

const iterator = store.values();
let item = iterator.next();

while (!item.done) {
  console.log(item.value);
  item = iterator.next();
};

where(predicate[, raw=false, op="||"])

Array

Ideal for when dealing with a composite index which contains an Array of values, which would make matching on a single value impossible when using find().

const store = haro(null, {key: 'guid', index: ['name', 'name|age', 'age']}),
   data = [{guid: 'abc', name: 'John Doe', age: 30}, {guid: 'def', name: 'Jane Doe', age: 28}];

store.batch(data, 'set');
console.log(store.where({name: 'John Doe', age: 30})); // [{guid: 'abc', name: 'John Doe', age: 30}]

Contributing

Contributions, issues, and feature requests are welcome! Feel free to check the issues page or submit a pull request.

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/my-feature)
  3. Commit your changes (git commit -am 'Add new feature')
  4. Push to the branch (git push origin feature/my-feature)
  5. Open a pull request

Support

For questions, suggestions, or support, please open an issue on GitHub, or contact the maintainer.

License

This project is licensed under the BSD-3 license - see the LICENSE file for details.

Changelog

See CHANGELOG.md for release notes and version history.