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

@byelabel/utils

v2.0.0

Published

ByeLabel Utilities

Downloads

2,948

Readme

@byelabel/utils

Server-side helpers: validators, error/log/data shapes, encryption, Sequelize/RabbitMQ/Redis bindings.

pnpm add @byelabel/utils
# or
npm install @byelabel/utils

Imports

Subpath imports are tree-shakable and recommended:

import { isUUID } from '@byelabel/utils/validator';
import { AppError, throwAppError } from '@byelabel/utils/error';
import { logError } from '@byelabel/utils/log';

The flat barrel re-exports everything. Modules whose names collide on connect / disconnect (db, rabbit, redis) are exposed as namespaces:

import { db, rabbit, redis, AppError, isUUID } from '@byelabel/utils';

await db.connect();
await rabbit.connect();
await redis.connect();

Modules

validator

Type guards for runtime shape checks. All return boolean.

| Function | True when... | | --- | --- | | isBuffer(v) | v instanceof Buffer | | isSymbol(v) | typeof symbol | | isUndefined(v) | undefined | | isNull(v) | null | | isArray(v) | array | | isNonEmptyArray(v) | array with at least one item | | isBoolean(v) | boolean | | isNumber(v) | typeof number (does not include numeric strings) | | isInteger(v) | numeric and integer | | isNumeric(v) | number or numeric string | | isString(v) | string | | isNonEmptyString(v) | string and not '' | | isObject(v) | plain object | | isNonNullObject(v) | plain object with at least one key | | isEmpty(v) / isNotEmpty(v) | empty by any of the above | | isFunction(v) | function or async function | | isAsyncFunction(v) | async function | | isUUID(v) | valid UUID string | | isEmail(v) | matches x@y | | isPhoneNumber(v) | matches a flexible international format | | isURL(v) | parseable http(s)://... URL | | isTCNumber(v) | valid Turkish national ID (11-digit, checksum) |

import { isUUID, isEmail, isNonEmptyArray } from '@byelabel/utils/validator';

isUUID('00000000-0000-4000-8000-000000000000'); // true
isEmail('[email protected]');                       // true
isNonEmptyArray([1, 2, 3]);                      // true

error

import { AppError, ResponseError, throwAppError, getErrorString } from '@byelabel/utils/error';

throw new AppError('Resource not found', 'NOT_FOUND', { id: '123' });

// auto-derives a code from the message
throw new AppError('Invalid token');           // code: 'INVALID_TOKEN'

// HTTP-flavored error
throw new ResponseError('Forbidden', 403);

// helper
throwAppError('Bad input', 'VALIDATION_ERROR', { field: 'email' });

// formatted "{message} - [{func} @ {file}:{line}:{col}]"
console.log(getErrorString(new Error('boom')));

data

import { paging, toResult, IList, IInfiniteList } from '@byelabel/utils/data';

paging(100);              // { offset: 0, limit: 20, total: 100 }
paging(50, 100, 10);      // { offset: 0, limit: 10, total: 50 } (offset clamped)
paging(5000, 0, 5000);    // { offset: 0, limit: 1000, total: 5000 } (limit capped)

toResult(undefined, { id: 1 });   // { success: true, payload: { id: 1 } }
toResult(new AppError('boom'));   // { success: false, error }
toResult(new Error('boom'));      // wraps as AppError('SYSTEM_ERROR')

crypt

import {
  createHash, comparePassword, createRandomHash, createKey,
  createToken, getRandomNumber, createSimpleHash
} from '@byelabel/utils/crypt';

createHash('hello');                     // HMAC-SHA256, secret = process.env.TOKEN_SECRET
createHash('hello', 'md5');              // simple md5
createHash('hello', 'sha256', 'mySec');  // explicit secret

comparePassword(savedHash, plain);       // boolean
createRandomHash(80);                    // hex string of length 80
createKey(6);                            // 6-digit numeric token, e.g. '482913'
createToken(5);                          // 5-char alphanumeric, e.g. 'A2K9F'
getRandomNumber(1, 100);                 // inclusive integer in [1, 100]

encryption

AES-256-GCM with auth tag, IV-prefixed binary output.

import { getKey, encrypt, decrypt } from '@byelabel/utils/encryption';

const key = getKey('my-password', 'my-salt');
const ciphertext = encrypt('secret message', key);   // Buffer
const plaintext  = decrypt(ciphertext, key);         // 'secret message'

log

File-rotated logging into ${ROOT_PATH}/${LOGS_PATH || 'logs'}/YYYY-MM-DD-{type}.log (UTC). Optional admin-email digest via sendMessage('message.send', ...) to the rabbit queue when SEND_LOGS=true.

import { logInfo, logWarning, logError, writeToFile, showMessages } from '@byelabel/utils/log';

await logInfo('app started');
await logWarning('cache miss', true);          // also console.log
await logError('Login failed', err, { userId });

writeToFile('boot complete', 'info');          // sync write, returns { path, name, fullPath }

showMessages([
  'PORT  : 3000',
  'NODE  : 20.x'
]);

number

Number formatting helpers built on Intl.NumberFormat. Exposed flat (toNumber, formatBytes, getRandom, ...) and namespaced (number.format, number.currency, number.short, number.percent) — the namespace form is recommended for the generic names. The formatter functions take a single human-readable options object (no raw Intl.NumberFormatOptions pass-through).

import { number, toNumber, formatBytes, getRandom } from '@byelabel/utils';
// or: import * as number from '@byelabel/utils/number';

toNumber('1.236', 2);                                      // 1.24 (rounded to 2 dp)
toNumber('abc');                                           // 0 (non-numeric → 0)

number.format(1234567.89);                                 // '1,234,567.89'
number.format(1234.5, { locale: 'tr-TR' });                // '1.234,5'
number.format(1.5, { decimals: 3 });                       // '1.500'
number.format(1234567.89, { grouping: false });            // '1234567.89'

number.currency(1234.5);                                   // '$1,234.50'
number.currency(10, 'EUR', { locale: 'en-US' });           // '€10.00'
number.currency(10.5, 'USD', { decimals: false });         // '$11'
number.currency(10.5, 'USD', { symbol: false });           // '10.50'
number.currencySymbol('GBP', { locale: 'en-US' });         // '£'

number.percent(25, { locale: 'en-US' });                   // '25%'   (input is already a percentage)
number.percent(25.5, { decimals: 2 });                     // '25.50%'

number.short(1500);                                        // '1.5K'
number.short(2_500_000);                                   // '2.5M'
number.short(1500, { long: true });                        // '1.5 thousand'
number.short(1234, { decimals: 2 });                       // '1.23K'

getRandom(1, 10);                                          // integer in [1, 10] inclusive

formatBytes(2048);                                         // '2.0 KB'
formatBytes(5 * 1024 * 1024);                              // '5.0 MB'

Options accepted by each formatter:

| Function | Options | | --- | --- | | format(n, options?) | locale, decimals (exact fraction digits), grouping (default true) | | currency(n, code?, options?) | locale, decimals (default true — currency-natural digits; false forces 0), symbol (default true) | | currencySymbol(code?, options?) | locale | | percent(n, options?) | locale, decimals (exact fraction digits) | | short(n, options?) | locale, decimals (max fraction digits, default 1), long (long compact display, default false) |

money

Currency-aware money formatting with built-in zero/three-decimal currency tables (e.g. JPY/KRW have 0, KWD/BHD have 3) and minor-unit (cents) conversion. Exposed as a namespace to avoid colliding with number.format.

import { money } from '@byelabel/utils';
// or: import * as money from '@byelabel/utils/money';

money.format(1234.5);                                  // '$1,234.50'
money.format(1500, { currency: 'JPY' });                   // '¥1,500'   (zero-decimal)
money.format(1.234, { currency: 'KWD', locale: 'en-US' }); // 'KWD 1.234' (three-decimal)
money.format(1234.5, { currency: 'EUR', locale: 'de-DE' });// '1.234,50 €'

money.format(1050, { minor: true });                   // '$10.50'   (1050 cents → $10.50)
money.format(10.5, { decimals: false });               // '$11'      (suppress fraction digits)
money.format(10.5, { symbol: false });                 // '10.50'    (no currency symbol)

money.toMinor(10.5);                                   // 1050   (USD → cents)
money.toMinor(1.234, 'KWD');                           // 1234   (3 decimals)
money.toMinor(1500, 'JPY');                            // 1500   (0 decimals, no scale)

money.toMajor(1050);                                   // 10.5
money.toMajor(1234, 'KWD');                            // 1.234

money.getCurrencyDecimals('USD');                      // 2
money.getCurrencyDecimals('JPY');                      // 0
money.getCurrencyDecimals('KWD');                      // 3

money.isZeroDecimalCurrency('JPY');                    // true
money.isZeroDecimalCurrency('USD');                    // false

format accepts MoneyFormatOptions:

| Option | Default | Notes | | --- | --- | --- | | currency | 'USD' | ISO 4217 currency code | | locale | 'en-US' | BCP 47 locale tag | | minor | false | Treat input as minor units (cents) and convert before formatting | | decimals | true | When false, fraction digits are forced to 0 | | symbol | true | When false, formats as plain decimal without the currency symbol |

dto

Joi schema validation + a generic data-mapping wrapper.

import { validateSchema, phoneNumberValidation } from '@byelabel/utils/dto';
import joi from 'joi';

const schema = joi.object({ email: joi.string().email().required() });

// sync — throws AppError('VALIDATION_ERROR') with details
const value = validateSchema(schema, { email: '[email protected]' });

// async
const value2 = await validateSchema(schema, { email: '[email protected]' }, true);

// custom Joi rule
joi.string().custom(phoneNumberValidation);

db

Sequelize wrapper with reconnection, replica support, and a typed filter/sort/paginate DSL.

import { db } from '@byelabel/utils';
// or: import * as db from '@byelabel/utils/db';

await db.connect();                                      // env-only
await db.connect({ name: 'analytics', debug: true });    // override per-field
const sequelize = db.sequelize();

// validate config without connecting (throws AppError if host/name/user missing)
db.checkDbConfig();
db.checkDbConfig({ host: 'db.local', name: 'app', user: 'app' });

// safe SQL string
db.escapeString("o'reilly", true);   // "'o''reilly'"

// build a WHERE from a structured filter
const where = await db.filtering([
  ['email', 'contains', 'gmail'],
  ['status', 'isAnyOf', ['active', 'pending']]
]);

// Joi schema for user-supplied filter input
const schema = db.JoiFilter('email', 'string');

// paginated list
const page = await db.getList<User>({
  offset: 0,
  limit: 20,
  sqlBody: async (alias, withColumns) => `SELECT ... FROM users AS "${alias}"`,
  dataModel: rows => rows
});

await db.disconnect();

connect(options?) and checkDbConfig(options?) accept an optional IDbOptions:

| Option | Env fallback | Default | | --- | --- | --- | | engine | DB_ENGINE | 'postgresql' | | host | DB_HOST | — (required) | | port | DB_PORT | 5432 | | name | DB_NAME | — (required) | | user | DB_USER | — (required) | | pass | DB_PASS | '' | | useSsl | DB_USE_SSL === 'true' | false | | prefix | DB_PREFIX | — | | skipSync | DB_SKIP_SYNC === 'true' | false | | forceSync | DB_FORCE_SYNC === 'true' | false | | debug | DB_DEBUG === 'true' | false |

checkDbConfig throws AppError (MISSING_DB_HOST, MISSING_DB_NAME, MISSING_DB_USER) when a required field is missing. Comma-separate host/port/user/pass for read replicas.

rabbit

RabbitMQ client with auto-reconnect (bounded retry-with-backoff), AMQP heartbeats, message streaming for payloads larger than messageMaxSize (default 5 MB), and request/response over reply queues.

import { rabbit } from '@byelabel/utils';

await rabbit.connect();                                            // env-only
await rabbit.connect({ host: 'mq.local', namespace: 'svc' });      // override per-field

// validate config without connecting (throws AppError if host missing)
rabbit.checkRabbitConfig();

// fire-and-forget
await rabbit.sendMessage('log.event', { action: 'login', user_id: id });

// request → reply
const result = await rabbit.sendMessageForReply('account.find', { id });

// listen
await rabbit.receiveMessage('account', async (params) => {
  return { success: true, payload: { /* ... */ } };
});

// pub/sub
await rabbit.publishMessage('user', 'updated', { id });
await rabbit.receivePublishedMessage('user', '*', payload => { /* ... */ });

// hook all queues + exchanges to the eventEmitter
await rabbit.listen(true);

connect(options?) and checkRabbitConfig(options?) accept an optional IRabbitOptions:

| Option | Env fallback | Default | | --- | --- | --- | | name | RABBIT_NAME | process.env.NAME'microservice' | | protocol | RABBIT_PROTOCOL | 'amqp' | | host | RABBIT_HOST | — (required) | | port | RABBIT_PORT | 5672 | | user | RABBIT_USER | — | | pass | RABBIT_PASS | — | | vhost | RABBIT_VHOST | — | | namespace | RABBIT_NAMESPACE | — | | messageMaxSize | RABBIT_MESSAGE_MAX_SIZE | 5000000 | | timeout | RABBIT_TIMEOUT | 30/60 (per call) | | queues | RABBIT_QUEUES | — (used by listen) | | exchanges | RABBIT_EXCHANGES | — (used by listen) | | heartbeat | RABBIT_HEARTBEAT | 60 (seconds; 0 disables) | | keepAlive | RABBIT_KEEP_ALIVE | true (set RABBIT_KEEP_ALIVE=false to disable TCP keepalive) | | keepAliveDelay | RABBIT_KEEP_ALIVE_DELAY | 10000 (ms; idle time before kernel sends keepalive probes) | | maxRetries | RABBIT_MAX_RETRIES | 5 (additional attempts after the first) | | retryDelay | RABBIT_RETRY_DELAY | 500 (ms; initial backoff, doubles per attempt) | | retryMaxDelay | RABBIT_RETRY_MAX_DELAY | 5000 (ms; backoff cap) |

The connection name advertised to RabbitMQ is ${name}-${pid}name is taken from the name option, falling back to RABBIT_NAME, then process.env.NAME, then 'microservice'. Each service shows up identifiable in the broker's connections view.

checkRabbitConfig throws AppError('MISSING_RABBIT_HOST') when host is missing.

redis

import { redis } from '@byelabel/utils';

const client = await redis.connect();                                  // env-only
const client2 = await redis.connect({ host: 'cache.local', db: 1 });   // override

// validate config without connecting (throws AppError if host missing)
redis.checkRedisConfig();

await client.set('key', 'value');
await redis.disconnect();

connect(options?) and checkRedisConfig(options?) accept an optional IRedisOptions:

| Option | Env fallback | | --- | --- | | host | REDIS_HOST (required) | | port | REDIS_PORT | | user | REDIS_USER | | pass | REDIS_PASS | | db | REDIS_DB |

checkRedisConfig throws AppError('MISSING_REDIS_HOST') when host is missing. Comma-separate host/port/user/pass to enable cluster mode.

events

Singleton wildcard EventEmitter2 plus an attachEvents helper that wires events/**/*.js files at a path.

import eventEmitter, { attachEvents, eventResult } from '@byelabel/utils/events';

eventEmitter.on('user.created', (payload, cb) => {
  cb(null, { id: payload.id });
});

await attachEvents(__dirname);   // loads ./events/*.js based on EVENTS env

jobs

Loads jobs/**/*.js and runs each export inside a fresh vm context.

import { runJobs } from '@byelabel/utils/jobs';

await runJobs(__dirname, { db, rabbit });

Filter set via JOBS=* or JOBS=foo,bar.run.

output

Express middleware helper. Wraps a callback into a structured { success, payload | error, transaction } response, with optional rabbit logging on LOG=request,response.

import { toResponse, responseData, IRequest } from '@byelabel/utils/output';

app.post('/login', (req, res) => {
  toResponse(req, res, async (transactionId) => {
    return { token: '...' };
  });
});

// manual shape
res.json(responseData(null, { token: '...' }));

config

Works in both Node and the browser. In Node it autoloads .env from process.cwd() and sets process.env.WORKING_PATH, ROOT_PATH, normalized ROUTE_PREFIX. In the browser the autoload is skipped (no filesystem) — loadEnv returns empty paths and reads ROUTE_PREFIX from whatever the bundler exposes.

import { loadEnv } from '@byelabel/utils/config';

// Node — read .env from cwd
const { rootPath, workingPath, routePrefix } = loadEnv();

// Node — named env file
loadEnv('.env.staging');

// Browser (Vite) — pass bundler-inlined envs
loadEnv('.env', { vars: import.meta.env });

// Anywhere — explicit override (file values still win in Node when both are present)
loadEnv('.env', { vars: { ROUTE_PREFIX: '/api/v1' } });

ROUTE_PREFIX is normalized: leading/trailing slashes are trimmed and collapsed, then a single leading / is added (api/v1//api/v1, ///''). In the browser, rootPath and workingPath are always '' — they have no meaning outside Node.

Repository

github.com/byelabel/helpers — see the workspace README for the companion @byelabel/react package.

License

MIT