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 🙏

© 2025 – Pkg Stats / Ryan Hefner

timezynk-registry-utils

v0.7.5

Published

Utilities for working with Timezynk registries.

Readme

npm

Timezynk Registry Utils

A utility library for building reference data from registry structures in Timezynk.

Features

  • Data Builder Factory: Creates functions that build reference data from registry items
  • Field Reference Resolution: Automatically resolves field references across registry structures
  • Dynamic Title Composition: Compose dynamic titles from multiple registry fields with custom separators
  • Formatter Utility: Built-in formatters for address, breaks, dates, and other complex field types
  • Caching: Built-in caching for performance optimization
  • Memoization Support: Export memoized builder factory for performance optimization

Installation

npm install timezynk-registry-utils

Basic Usage

import { dataBuilderFactory, memoizedDataBuilderFactory } from 'timezynk-registry-utils';

// Basic usage
const refDataBuilder = dataBuilderFactory(fieldInstances, registryData, users);
const refData = refDataBuilder(shift);

// With memoization (recommended for performance)
const refDataBuilder = memoizedDataBuilderFactory(fieldInstances, registryData, users);
const refData = refDataBuilder(shift);

Dynamic Title Composition

The library supports dynamic title composition that can combine multiple registry fields into a single title string. This is particularly useful for creating searchable, human-readable titles from complex registry data.

Note: Dynamic title composition is only applied to shift registry items (items that have a booked-users field). This ensures that time reports and other registry types preserve their original titles.

Enhanced Features

This implementation is based on the original utility but includes several improvements:

  • Registry Reference Support: Automatically resolves formatId: "registry-reference" by looking up titles from the path property
  • Separator-Only Detection: Prevents malformed titles like ", , " by returning empty string when composition results in separator-only strings
  • Performance Optimization: Uses memoization to avoid recreating title builders unnecessarily
  • Graceful Fallbacks: Handles missing registry references gracefully without breaking title composition
  • Original Behavior Preservation: Maintains the original utility's behavior where field-based composition takes precedence over path-based fallbacks

Supported Title Composition Methods

This library handles the following title composition scenarios:

  1. Field-Based Composition: Combines values from specific registry fields using a custom separator
  2. Path-Based Composition: Uses registry reference paths to build hierarchical titles (fallback only when no fields configured)
  3. Separator-Only Detection: Returns empty string when composition results in separator-only strings (no fallback to path)

Additional Title Composition Use Cases

The following title composition scenarios are handled separately in the main application (tzcontrol) and are not processed by this library:

1. Shift Title from Order (shiftTitleFromOrder)

Creates titles from order form fields and supplier information:

if (item.status === 'order-outgoing') {
    title = shiftTitleFromOrder(item, form, supplier);
}

2. Shift Title from RFQ (shiftTitleFromRFQ)

Creates titles from RFQ (Request for Quote) relations:

if (item.getIn?.(['relations', 'incoming-rfq-id'])) {
    title = shiftTitleFromRFQ(item);
}

Important: These additional title composition methods are intentionally kept separate from this library to:

  • Prevent additional Redux store dependencies
  • Avoid increasing registry utility complexity
  • Keep UI-specific title logic in the main application
  • Maintain clear separation of concerns

These methods are used for UI display purposes only and should not be expected to be processed in the refData object.

Formatter Utility

The library includes a comprehensive formatter utility that handles complex field value formatting. This is particularly useful for dynamic title composition and other data transformation needs.

Available Formatters

| Formatter ID | Description | Example Input | Example Output | | ------------ | ---------------------------------------------------- | ------------------------------------------------------- | ------------------------------ | | address | Formats address objects with proper comma separation | {address1: "Main St", city: "Stockholm", country: 46} | "Main St, Stockholm, Sweden" | | breaks | Formats time breaks as time ranges | [{start: "10:00", end: "12:00"}] | "10:00-12:00" | | start-end | Formats start/end date pairs | ["2025-01-21", "2025-01-22"] | "2025-01-21, 10:00" | | boolean | Formats boolean values as Yes/No | true | "Yes" | | standard | Basic string formatting (default) | " hello " | "hello" |

Peer Dependencies for Formatters

Some formatters require additional peer dependencies to be installed by the consuming application:

npm install dateformat-light tzdateutils

Required for:

  • breaks formatter (time formatting)
  • start-end formatter (date formatting)

Not required for:

  • address formatter
  • boolean formatter
  • standard formatter

If the peer dependencies are not installed, these formatters will fall back to basic string conversion.

Configuration Structure

The dynamic title settings follow this structure:

{
    "id": "553e2f1f3029e0478fc757f2/dynamic-title", // registry-id/dynamic-title
    "value": {
        "separator": " » ", // Custom separator between field values
        "fields": [
            {
                "formatId": "standard", // Standard field formatter
                "id": "title-6894be7bca96a32dabf1fd96" // Field ID to include
            },
            {
                "formatId": "registry-reference", // Registry reference formatter
                "id": "6894be8a6d3f9a793f88a958" // Field ID that references another registry
            }
        ]
    }
}

Field Format Types

  • standard: Uses the field value directly or applies a custom formatter if defined
  • registry-reference: Looks up the title from the referenced registry item in the path property
  • Custom formatters: Can be defined in the registry fields using the formatter property

Basic Title Composition

import { dataBuilderFactory } from 'timezynk-registry-utils';
import { defaultRegisters } from 'timezynk-registry-utils';

// Define your fields
const fields = Immutable.Map({
    FIELD_A: Immutable.Map({
        id: 'FIELD_A',
        'field-id': 'field-a',
        'field-type': 'text',
        'field-section': 'generic',
    }),
    FIELD_B: Immutable.Map({
        id: 'FIELD_B',
        'field-id': 'field-b',
        'field-type': 'text',
        'field-section': 'generic',
    }),
});

// Configure title composition settings
const dynamicTitleSetting = Immutable.fromJS({
    id: `${defaultRegisters.SHIFTS_REG_ID}/dynamic-title`,
    value: {
        separator: ' - ',
        fields: [{ id: 'FIELD_A' }, { id: 'FIELD_B' }],
    },
});

// Create data builder with title composition enabled
const dataBuilder = dataBuilderFactory(fields, registryData, users, undefined, undefined, dynamicTitleSetting);

// Use the data builder
const refData = dataBuilder(
    Immutable.Map({
        id: 'ITEM1',
        'registry-id': 'SHIFTS',
        values: Immutable.Map({
            FIELD_A: 'Value A',
            FIELD_B: 'Value B',
        }),
    })
);

// Result: refData.get('title') === "Value A - Value B"

Registry Reference Title Composition

When a field in your dynamic title settings has formatId: "registry-reference", the system will automatically look up the title from the referenced registry item in the path property:

const dynamicTitleSetting = Immutable.fromJS({
    id: `${defaultRegisters.SHIFTS_REG_ID}/dynamic-title`,
    value: {
        separator: ' » ',
        fields: [
            {
                formatId: 'registry-reference',
                id: '68b710c6bda6d25184246fd9', // Field that references another registry
            },
        ],
    },
});

// This will compose the title using the referenced registry's title from the path
// Result: "Referenced Registry Title" instead of just the registry ID

**Note**: If the referenced registry item is not found in the `path`, the system falls back to using the raw field value (e.g., the registry ID) to ensure the title composition doesn't fail.

### Path-Based Title Composition (Fallback)

When no specific fields are configured or when fields list is empty, the system falls back to path-based title composition:

```typescript
const settings = Immutable.fromJS({
    id: `${defaultRegisters.SHIFTS_REG_ID}/dynamic-title`,
    value: {
        separator: ' | ',
        fields: [], // Empty fields list triggers path-based composition
    },
});

// This will use the path data (registry references) or fall back to the item's title
// Result: "Parent Registry Title | Child Registry Title"

Manual Title Composition

You can also use the title composition functions directly:

import { composeTitle, createTitleBuilder } from 'timezynk-registry-utils';

// Compose title from existing refData
const composedTitle = composeTitle(refData, undefined, settings, registryFields);

// Create a reusable title builder
const titleBuilder = createTitleBuilder(settings, registryFields);
const title = titleBuilder(refData);

Note: composeTitle is the main entry point that handles both field-based and path-based title composition automatically based on your settings configuration.

Important: In most cases, you should get the title directly from refData.get('title') rather than calling composeTitle manually. The dataBuilderFactory automatically applies title composition when building refData, so React components and other consumers should simply use the pre-computed title from the refData object.

Behavior When Field-Based Composition Fails

When field-based composition is configured but fails (e.g., all fields are empty or result in separator-only strings):

  • Returns empty string '' instead of falling back to path-based composition
  • No fallback to original title - the empty string is used as-is
  • Maintains original utility behavior where field-based composition takes precedence

This ensures that when you configure specific fields for title composition, the system respects your configuration and doesn't unexpectedly fall back to path-based or original titles.

Integration with Redux Store

For applications using Redux, you can create a connected data builder:

import { defaultMemoize } from 'reselect';
import { dataBuilderFactory } from 'timezynk-registry-utils';
import store from 'state/store';
import { getAllRegistryFields, getRegistryData, getAllUsers, getCompanySetting } from 'state/selectors';
import { defaultRegisters } from 'timezynk-registry-utils';

const SHIFT_TITLE_SETTING_ID = `${defaultRegisters.SHIFTS_REG_ID}/dynamic-title`;

// Memoized builder factory
const getDataBuilder = defaultMemoize(dataBuilderFactory);

interface RefDataBuilderOptions {
    dynamicTitle?: boolean;
}

export function createConnectedRefDataBuilder(options: RefDataBuilderOptions = {}) {
    return function (item) {
        const state = store.getState();
        const registryFields = getAllRegistryFields(state);
        const registryData = getRegistryData(state);
        const users = getAllUsers(state);

        if (options.dynamicTitle) {
            const titleSettings = getCompanySetting(state, SHIFT_TITLE_SETTING_ID);

            return getDataBuilder(registryFields, registryData, users, undefined, undefined, titleSettings)(item);
        }

        return getDataBuilder(registryFields, registryData, users)(item);
    };
}

// Export for direct usage
export { getDataBuilder };

// Usage
const refDataBuilder = createConnectedRefDataBuilder({ dynamicTitle: true });
const refData = refDataBuilder(item);

API Reference

dataBuilderFactory

Creates a function that builds reference data from registry items.

function dataBuilderFactory(
    regFields: Immutable.Map<string, FieldInstance> | undefined,
    regData: Immutable.Map<string, RegistryDataInstance>,
    users: Immutable.Map<string, User>,
    invoiceArticles?: Immutable.Map<string, InvoiceArticle>,
    salaryArticles?: Immutable.Map<string, SalaryArticle>,
    dynamicTitleSetting?: Immutable.Map<string, any> | any
): DataBuilder;

Parameters:

  • regFields: Registry field definitions
  • regData: Registry data instances
  • users: User data
  • invoiceArticles: Optional invoice articles data
  • salaryArticles: Optional salary articles data
  • dynamicTitleSetting: Optional dynamic title composition settings

memoizedDataBuilderFactory

A memoized version of dataBuilderFactory for performance optimization.

const memoizedDataBuilderFactory = defaultMemoize(dataBuilderFactory);

composeTitle

Composes a title from reference data using configured settings.

function composeTitle(
    data: RefData,
    removeId?: string,
    settings?: Immutable.Map<string, any> | any,
    regFields?: Immutable.Map<string, FieldInstance>
): string | null;

Parameters:

  • data: The reference data object
  • removeId: Optional registry ID to exclude from path-based composition
  • settings: Dynamic title composition settings
  • regFields: Registry field definitions for formatter support

createTitleBuilder

Creates a reusable title builder function from settings.

function createTitleBuilder(
    settings: Immutable.Map<string, any> | any,
    regFields: Immutable.Map<string, FieldInstance>
): (refData: RefData, removeId?: string) => string | null;

getFormatter

Gets a formatter function by ID for formatting complex field values.

function getFormatter(id: string): FormatterFunction;

Parameters:

  • id: The formatter ID (e.g., 'address', 'breaks', 'start-end', 'boolean', 'standard')

Available Formatters:

  • address: Formats address objects with proper comma separation
  • breaks: Formats time breaks as "HH:MM-HH:MM"
  • start-end: Formats start/end date pairs
  • boolean: Formats boolean values as "Yes"/"No"
  • standard: Basic string formatting (default)

Example:

import { getFormatter } from 'timezynk-registry-utils';

const addressFormatter = getFormatter('address');
const formattedAddress = addressFormatter({
    address1: 'Main Street 123',
    city: 'Stockholm',
    country: 46,
});
// Result: "Main Street 123, Stockholm, Sweden"

Best Practices

Using Titles in React Components

✅ Recommended: Get title from refData

const MyComponent = ({ item }) => {
    const refData = dataBuilder(item);
    const title = refData.get('title'); // Pre-computed by dataBuilderFactory

    return <div>{title}</div>;
};

❌ Not recommended: Manual title composition

const MyComponent = ({ item, settings, regFields }) => {
    const refData = dataBuilder(item);
    // Don't do this - title is already computed in refData
    const title = composeTitle(refData, undefined, settings, regFields);

    return <div>{title}</div>;
};

The dataBuilderFactory automatically handles title composition during refData creation, so consumers should use the pre-computed title field rather than calling title composition functions directly.

How Path Works

The path property in refData contains a hierarchical trail of registry references:

// Example path structure
{
  "path": [
    {
      "title": "Parent Registry Item",
      "id": "parent-id",
      "registry-id": "parent-registry-id"
    },
    {
      "title": "Child Registry Item",
      "id": "child-id",
      "registry-id": "child-registry-id"
    }
  ],
  "title-parent-registry-id": "Parent Registry Item",
  "title-child-registry-id": "Child Registry Item"
}
  • path: Contains registry references (items that reference other registry items)
  • title-{registry-id}: Created for each registry reference to store the referenced item's title
  • Direct field values: Appear at the root level for regular fields

Configuration

Title Composition Settings

The title composition is configured through settings with the key pattern: ${registryId}/dynamic-title

{
    separator: string,           // Separator between field values (default: ', ')
    fields: Immutable.List([     // List of fields to compose
        { id: 'FIELD_ID' },      // Field ID to include
        { id: 'FIELD_ID', formatId: 'FORMATTER_ID' } // With optional formatter
    ])
}

Fallback Behavior:

  • Fields configured: Uses field-based composition exclusively, no path fallback
  • No fields configured: Falls back to path-based composition
  • No settings: No title composition applied

Field Formatters

Formatters can be defined in the registry fields themselves using the formatter property:

const fieldWithFormatter = Immutable.Map({
    id: 'FIELD_ID',
    'field-id': 'field-name',
    'field-type': 'text',
    formatter: (value) => `Formatted: ${value}`, // Custom formatter function
});

Examples

See the test files for comprehensive examples of how to use the library:

  • src/dataBuilderFactory.test.ts - Core functionality tests
  • src/defaultValue.test.ts - Default value handling
  • src/salary.test.ts - Salary-specific functionality

Migration from Legacy dataBuilder

If you're migrating from the legacy dataBuilder pattern:

// Legacy pattern (deprecated)
import { makeDataBuilder } from 'state/selectors';
import { setDataBuilder } from '@timezynk/tzstores';
import store from 'state/store';

let currentBuilder;
export default function dataBuilder(item) {
    return currentBuilder?.(item);
}

// New pattern (recommended)
import { createConnectedRefDataBuilder } from './your-new-file';

const refDataBuilder = createConnectedRefDataBuilder({ dynamicTitle: true });
const refData = refDataBuilder(item);