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

@docyrus/app-utils

v0.11.0

Published

Utility functions for frontend applications using Docyrus as backend

Downloads

1,563

Readme

@docyrus/app-utils

Framework-agnostic utility functions for frontend applications using Docyrus as backend. Provides date/number formatting, template compilation (Handlebars + JSONata), and a comprehensive set of expression helpers — all configured from tenant preferences.

Features

  • Tenant Preferences: Fetch tenant configuration from the Docyrus API
  • Date Formatting: PHP-style date formatting with timezone support (Y-m-d, H:i:s, etc.)
  • Number Formatting: Locale-aware number formatting with custom separators and precision
  • Number to Words: Convert numbers to words in Turkish and English with currency labels
  • Duration Formatting: Format seconds as HH:MM, decimal hours, or human-readable words
  • App Config: Get, upsert, and delete a per-app JSON configuration (1:1 per app)
  • User App Config: Get, upsert, and delete a per-user JSON configuration scoped to an app (1:1 per user per app)
  • Data Views: CRUD for saved data source view configurations (columns, filters, sort, color rules)
  • Data Forms: CRUD for dynamic form designs attached to a data source (layout JSON, title, icon, color, default flag, soft-archive)
  • Data Source Metadata: Read data source metadata across apps, by app, by ID, or by slug with optional expansions like fields
  • TanStack Query Integration (optional, via @docyrus/app-utils/query): Prebuilt queryOptions factories and a query-key registry for data source metadata, with reactive caching and per-resource invalidation helpers
  • Envelope Unwrapping: envelopeUnwrapInterceptor (a RestApiClient interceptor) and unwrapEnvelope (a per-call helper) for handling Docyrus' { success, data } API envelope — both shape-strict so domain payloads with their own data column stay safe
  • Template Engine: Handlebars compilation with async support and inline JSONata formulas
  • Formula Evaluation: JSONata expression evaluation with 70+ built-in helper functions
  • JSONata Helpers: Date arithmetic (date-fns), string manipulation, number formatting, equality checks, and more
  • TypeScript: Full type definitions included
  • Framework-Agnostic: No Vue, React, or Angular dependency — works everywhere

Installation

npm install @docyrus/app-utils
pnpm add @docyrus/app-utils

Peer Dependencies

| Package | Version | Required | |---------|---------|----------| | @docyrus/api-client | >= 0.1.0 | Yes (for getTenantPreferences) | | handlebars | >= 4.7.0 | Yes (for template engine) | | jsonata | >= 2.0.0 | Yes (for formula evaluation) | | date-fns | >= 3.0.0 | Yes (for JSONata date helpers) | | @tanstack/query-core | >= 5.17.0 | Optional (only for @docyrus/app-utils/query) |

Quick Start

import {
  getTenantPreferences,
  createDateUtils,
  createNumberUtils,
  createTemplateEngine,
  createDataSourceClient
} from '@docyrus/app-utils';

// 1. Fetch tenant preferences
const preferences = await getTenantPreferences(apiClient);

// 2. Create utility instances
const dateUtils = createDateUtils({
  preferences,
  userTimezone: currentUser.timeZone?.id // e.g. 'Europe/Istanbul'
});

const numberUtils = createNumberUtils({ preferences });
const dataSources = createDataSourceClient(apiClient);

// 3. Create template engine (optional)
const engine = createTemplateEngine({
  dateUtils,
  numberUtils,
  user: currentUser,
  extraJsonataBindings: { hasRole }
});

// 4. Use utilities
dateUtils.formatDate('2024-01-15');           // '15.01.2024' (tenant format)
numberUtils.formatNumber(1234.5);             // '1.234,50'   (tenant locale)
await engine.compileFormula('$sum(items.price)', data);

// 5. Read app and data source metadata
const apps = await dataSources.listApps();
const sources = await dataSources.list({ expand: 'fields' });
const contacts = await dataSources.getBySlug('crm', 'contacts', {
  expand: 'fields'
});

// 6. Use metadata
const appSlugs = apps.map(app => app.slug);
const fieldSlugs = contacts.fields?.map(field => field.slug) ?? [];
const requiredFields = contacts.fields?.filter(field => field.required) ?? [];

Envelope handling

Docyrus REST endpoints wrap their payload in a { success, data } envelope (optionally with meta for pagination, or error / message for controller-level errors). @docyrus/app-utils provides two ways to handle that — both are tolerant of pre-stripped envelopes (i.e. idempotent), and both refuse to falsely unwrap domain payloads that carry their own data field (AppConfig, UserAppConfig, records with a data column).

envelopeUnwrapInterceptor — global, conservative

A RestApiClient response interceptor. After it runs, client.get/post/put/delete returns the inner payload directly (so generated TanStack DB collections, hand-written calls, and app-utils clients all see bare values). Required when using @docyrus/tanstack-db-generator collections — the generator emits methods that don't read .data themselves.

import { RestApiClient } from '@docyrus/api-client';
import { envelopeUnwrapInterceptor } from '@docyrus/app-utils';

const client = new RestApiClient({ baseURL: 'https://api.example.com' });
client.addInterceptor(envelopeUnwrapInterceptor);

// Now plain envelopes unwrap automatically:
const views = await client.get<DataView[]>('/v1/apps/crm/data-sources/contacts/views');
//      ^ DataView[] (from { success, data: [...] })

It's deliberately shape-strict and lossless — it strips only when the envelope's only top-level keys are data and optionally success. Envelopes that carry sidecar information are left intact so callers can read them:

| Response shape | Behaviour | Why | |---|---|---| | { data } | unwrap → data | Bare envelope. | | { success, data } | unwrap → data | Most endpoints. | | { data, meta } / { success, data, meta } | left alone | Paginated lists — meta.count/meta.total needed by data grids. | | { success: false, data, error, message } | left alone | Controller-level error envelope — error info preserved. | | { id, data, status, tenant_app_id, … } | left alone | Domain record (e.g. AppConfig) carrying its own data column. |

For paginated endpoints, read both the array and the page total off the envelope:

const { data: rows, meta } = await client.get<{
  data: Record[];
  meta?: { count?: number; total?: number };
}>('/v1/apps/crm/data-sources/contacts/items');

unwrapEnvelope<T>(value) — per-call, aggressive

A small helper for ad-hoc unwrap at known sites. It's wider than the interceptor — it also strips envelopes carrying meta / error / message because the caller has explicitly typed the return as T and isn't surfacing sidecar fields. This is exactly what app-utils' built-in clients (createDataSourceClient, createDataViewClient, createDataFormClient, createAppConfigClient, createUserAppConfigClient) use internally, so they keep working whether or not you've registered the interceptor globally.

import { unwrapEnvelope } from '@docyrus/app-utils';

const records = unwrapEnvelope<Record[]>(
  await client.get('/v1/apps/crm/data-sources/contacts/items')
);
// → Record[] (meta intentionally discarded)

Domain payloads with a data column are still safe — the closed-set check ({ data, success, meta, error, message }) rejects records that carry off-set keys like id / status.

App Config & Data Views

createAppConfigClient(client, appId)

Manages the single JSON configuration object for an app (1:1 relationship — no config IDs).

import { createAppConfigClient } from '@docyrus/app-utils';

const config = createAppConfigClient(apiClient, appId);

config.get()

Returns the app's configuration. Throws 404 if none exists.

const appConfig = await config.get();
// { id: '...', data: { theme: 'dark', ... }, status: 1, tenant_app_id: '...', ... }

config.upsert(body)

Creates the config if it doesn't exist, or updates it if it does.

const updated = await config.upsert({
  data: { theme: 'dark', sidebar: { collapsed: false } },
  status: 1
});

config.remove()

Hard deletes the config (irreversible).

await config.remove();

createUserAppConfigClient(client, appId)

Manages the per-user JSON configuration object for an app (1:1 per user per app). Useful for storing user preferences, UI state, or personalisation that should persist per user independently of the shared app config.

import { createUserAppConfigClient } from '@docyrus/app-utils';

const userConfig = createUserAppConfigClient(apiClient, appId);

userConfig.get()

Returns the current user's configuration for the app. Throws 404 if none exists.

const config = await userConfig.get();
// { id: '...', data: { theme: 'dark', ... }, status: 1, tenant_app_id: '...', user_id: '...', ... }

userConfig.upsert(body)

Creates the config if it doesn't exist, or updates it if it does.

const updated = await userConfig.upsert({
  data: { theme: 'dark', sidebarCollapsed: true },
  status: 1
});

userConfig.remove()

Hard deletes the user config (irreversible).

await userConfig.remove();

createDataViewClient(client, appSlug, dataSourceSlug)

Manages saved view configurations for a specific data source (1:many). Views define columns, filters, sorting, color rules, and quick-filter shortcuts.

import { createDataViewClient } from '@docyrus/app-utils';

const dataViews = createDataViewClient(apiClient, 'my-app', 'my-data-source');

dataViews.list(params?)

Returns all non-archived views. Optionally pass appId to filter views configured for a specific app (different from the data source's owning app).

const views = await dataViews.list();
const forSpecificApp = await dataViews.list({ appId: 'other-app-uuid' });

dataViews.get(viewId)

Returns a single non-archived view by ID.

const view = await dataViews.get('view-uuid');

dataViews.create(body)

Creates a new data view. name is required. The data source is determined by the dataSourceSlug passed to the client factory.

const view = await dataViews.create({
  name: 'High Priority',
  columns: { visible: ['title', 'priority', 'assignee'] },
  filters: { priority: 'high' },
  sort: { field: 'due_date', direction: 'asc' },
  color: '#E74C3C',
  icon: 'alert-triangle',
  is_default: false,
  sort_order: 2
});

dataViews.update(viewId, body)

Partially updates a view. Use archived: true to soft-delete.

await dataViews.update('view-uuid', { name: 'Renamed View' });
await dataViews.update('view-uuid', { archived: true }); // soft-delete

dataViews.remove(viewId)

Hard deletes a view (irreversible).

await dataViews.remove('view-uuid');

createDataFormClient(client, appSlug, dataSourceSlug)

Manages dynamic form designs attached to a data source (1:many). Forms describe the editable layout used to view and edit records of a data source — title, icon, color, the layout JSON itself (sections / fields / tabs), default selection, and soft-archive state.

import { createDataFormClient } from '@docyrus/app-utils';

const dataForms = createDataFormClient(apiClient, 'crm', 'contacts');

dataForms.list()

Returns all non-archived forms for the data source, ordered by created_on ascending.

const forms = await dataForms.list();

dataForms.get(formId)

Returns a single non-archived form by ID. Throws 404 if the form does not exist or is archived.

const form = await dataForms.get('form-uuid');

dataForms.create(body)

Creates a new form. name is required; everything else is optional. The data source is determined by the dataSourceSlug passed to the client factory — tenant_data_source_id, tenant scope, and the created_by audit fields are populated server-side.

const form = await dataForms.create({
  name: 'Compact contact form',
  title: 'Contact',
  icon: 'user',
  color: '#4F46E5',
  layout: {
    sections: [
      { id: 'main', fields: ['name', 'email', 'phone'] }
    ]
  },
  is_default: false,
  status: 1
});

dataForms.update(formId, body)

Partially updates a form. Only the fields present in the body are written; omitted fields are preserved. layout is replaced wholesale when provided. Use archived: true to soft-archive (the row is then excluded from list/get).

await dataForms.update('form-uuid', {
  title: 'Customer Contact',
  icon: 'address-book',
  layout: { sections: [/* updated sections */] }
});

await dataForms.update('form-uuid', { archived: true }); // soft-delete

dataForms.remove(formId)

Hard deletes a form (irreversible). Use update(formId, { archived: true }) instead if you want a recoverable soft-delete.

await dataForms.remove('form-uuid');

createDataSourceClient(client)

Reads Docyrus app and data source metadata across apps or for a specific app. This is useful when you need schema-aware UI behavior such as loading available apps, reading field definitions, or resolving a data source by slug.

import { createDataSourceClient } from '@docyrus/app-utils';

const dataSources = createDataSourceClient(apiClient);

Expand options

The metadata endpoints support comma-separated expansions via expand.

Available values:

  • children
  • fields
  • forms
  • acl
  • templates
  • client-configuration
  • misc

Example:

const sources = await dataSources.list({
  expand: 'fields,forms'
});

dataSources.listApps()

Lists apps available to the current tenant.

const apps = await dataSources.listApps();
// [{ id: 'uuid', name: 'CRM', slug: 'crm', logo_url: '...', status: 'active' }]

dataSources.list(params?)

Lists all data sources the current tenant can access across all apps.

const all = await dataSources.list();
const allWithFields = await dataSources.list({ expand: 'fields' });

dataSources.listByAppSlug(appSlug, params?)

Lists all data sources for a specific app slug.

const crmSources = await dataSources.listByAppSlug('crm');
const crmWithFields = await dataSources.listByAppSlug('crm', {
  expand: 'fields,acl'
});

dataSources.listByAppId(appId, params?)

Lists all data sources for a specific app ID.

const appSources = await dataSources.listByAppId('app-uuid');
const appSourcesWithFields = await dataSources.listByAppId('app-uuid', {
  expand: 'fields'
});

dataSources.getById(dataSourceId, params?)

Returns a single data source metadata object by data source ID.

const contacts = await dataSources.getById('data-source-uuid');
const contactsWithFields = await dataSources.getById('data-source-uuid', {
  expand: 'fields'
});

dataSources.getBySlug(appSlug, dataSourceSlug, params?)

Returns a single data source metadata object by app slug and data source slug.

const contacts = await dataSources.getBySlug('crm', 'contacts');
const contactsWithFields = await dataSources.getBySlug('crm', 'contacts', {
  expand: 'fields,forms'
});

Response shape

Basic responses include fields such as:

{
  id: 'uuid',
  name: 'Contacts',
  slug: 'contacts',
  appSlug: 'crm'
}

When expand=fields is used, the response can include fields metadata as well.


createDataSourceQueries(client) — TanStack Query integration

Returns prebuilt queryOptions-style factories and a query-key registry, so you get caching, deduplication, and reactive auto-invalidation for free. Works with @tanstack/react-query, @tanstack/vue-query, @tanstack/solid-query, etc. — only @tanstack/query-core is required at runtime.

Imported from a separate entrypoint to keep the optional @tanstack/query-core peer dep out of the main bundle:

import { createDataSourceQueries } from '@docyrus/app-utils/query';

const queries = createDataSourceQueries(apiClient);

Usage with React

import { useQuery, useQueryClient } from '@tanstack/react-query';
import { createDataSourceQueries } from '@docyrus/app-utils/query';

const queries = createDataSourceQueries(apiClient);

function ContactsTable() {
  const { data: contacts } = useQuery(
    queries.getBySlugOptions('crm', 'contacts', { expand: 'fields,forms' })
  );

  const queryClient = useQueryClient();
  const handleSchemaChange = () => queries.invalidateBySlug(queryClient, 'crm', 'contacts');

  // ...
}

Available *Options factories

Each factory mirrors the corresponding createDataSourceClient method and produces a { queryKey, queryFn } object you pass straight to useQuery (or useSuspenseQuery, prefetchQuery, etc.):

| Factory | Wraps | |---------|-------| | listAppsOptions() | dataSources.listApps() | | listOptions(params?) | dataSources.list(params) | | listByAppSlugOptions(appSlug, params?) | dataSources.listByAppSlug(...) | | listByAppIdOptions(appId, params?) | dataSources.listByAppId(...) | | getByIdOptions(dataSourceId, params?) | dataSources.getById(...) | | getBySlugOptions(appSlug, dataSourceSlug, params?) | dataSources.getBySlug(...) |

Per-request options (staleTime, enabled, select, placeholderData, …) are passed at the call site:

useQuery({
  ...queries.listOptions({ expand: 'fields' }),
  staleTime: 5 * 60 * 1000,
  enabled: isAuthenticated
});

Invalidation helpers

All take a QueryClient so consumers can call them anywhere (event handlers, mutation onSuccess, etc.).

| Helper | Invalidates | |--------|-------------| | invalidateAll(qc) | Every cached data-source query (apps + lists + details) | | invalidateApps(qc) | Cached app list | | invalidateLists(qc) | All list* variants (across apps and params) | | invalidateDetails(qc) | All getById / getBySlug results | | invalidateById(qc, dataSourceId) | Every cached detail for a single data source ID (any expand) | | invalidateBySlug(qc, appSlug, dataSourceSlug) | Every cached detail for a single app+slug pair (any expand) | | invalidateForAppSlug(qc, appSlug) | All list queries scoped to an app slug | | invalidateForAppId(qc, appId) | All list queries scoped to an app ID |

import { useMutation, useQueryClient } from '@tanstack/react-query';

const queryClient = useQueryClient();

const updateSchema = useMutation({
  mutationFn: (changes) => api.patchSchema(changes),
  onSuccess: () => queries.invalidateBySlug(queryClient, 'crm', 'contacts')
});

dataSourceKeys registry

The same query keys used internally are exported as dataSourceKeys (and as queries.keys). Use them when you need to read or set cache entries directly:

import { dataSourceKeys } from '@docyrus/app-utils/query';

queryClient.setQueryData(
  dataSourceKeys.bySlug('crm', 'contacts', { expand: 'fields' }),
  optimisticData
);

The key shape is stable: every key starts with ['docyrus', 'data-sources', ...], so queryClient.invalidateQueries({ queryKey: dataSourceKeys.all() }) clears everything this module owns.


API Reference

getTenantPreferences(client)

Fetches tenant preferences from the Docyrus API.

import { getTenantPreferences } from '@docyrus/app-utils';

const preferences = await getTenantPreferences(apiClient);
// { date_format: 'd.m.Y', decimal_separator: ',', thousand_separator: '.', ... }

Parameters:

  • client — A configured RestApiClient from @docyrus/api-client

Returns: Promise<TenantPreferences>


createDateUtils(config)

Creates date formatting utilities configured from tenant preferences.

import { createDateUtils } from '@docyrus/app-utils';

const dateUtils = createDateUtils({
  preferences,
  userTimezone: 'Europe/Istanbul' // defaults to 'UTC'
});

dateUtils.formatDate(date, options?)

Formats a date using the tenant's date_format (default Y-m-d).

dateUtils.formatDate('2024-06-15');                         // uses tenant format
dateUtils.formatDate(new Date(), { format: 'd/m/Y' });     // custom format
dateUtils.formatDate('2024-06-15', { timezone: 'US/Eastern' }); // custom timezone

dateUtils.formatDateTime(date, options?)

Formats a datetime using the tenant's date_time_format (default Y-m-d H:i:s). Handles timezone-aware parsing — appends Z to strings without a timezone suffix.

dateUtils.formatDateTime('2024-06-15T14:30:00');     // parses as UTC
dateUtils.formatDateTime('2024-06-15T14:30:00+03:00'); // respects offset

dateUtils.formatDateLong(date, options?)

Formats a date using the tenant's long_date_format (default Y-m-d H:i:s).

dateUtils.toUserTimezone(date)

Converts a date to the user's timezone without formatting.

const localDate = dateUtils.toUserTimezone('2024-06-15T12:00:00Z');

Format strings use PHP date format syntax (via php-date-formatter): | Token | Output | Example | |-------|--------|---------| | Y | 4-digit year | 2024 | | m | Month (01-12) | 06 | | d | Day (01-31) | 15 | | H | Hours 24h (00-23) | 14 | | i | Minutes (00-59) | 30 | | s | Seconds (00-59) | 00 | | D | Short day name | Sat | | l | Full day name | Saturday | | F | Full month name | June |


createNumberUtils(config)

Creates number formatting utilities configured from tenant preferences.

import { createNumberUtils } from '@docyrus/app-utils';

const numberUtils = createNumberUtils({ preferences });

numberUtils.formatNumber(value, options?)

Formats a number using the tenant's locale, separators, and precision.

numberUtils.formatNumber(1234567.89);
// With tenant settings: thousand_separator='.', decimal_separator=',', decimal_precision=2
// → '1.234.567,89'

// Override per call:
numberUtils.formatNumber(1234.5, {
  decimalPrecision: 3,
  decimalSeparator: '.',
  thousandSeparator: ','
});
// → '1,234.500'

Formatting logic:

  • If both thousandSeparator and decimalSeparator are set → manual formatting with regex
  • Otherwise → toLocaleString() with the tenant's locale
  • Pass thousandSeparator: '' to disable grouping

formatNumberToWords(value, options?)

Converts a number to words with currency labels. Standalone function (no config needed).

import { formatNumberToWords } from '@docyrus/app-utils';

formatNumberToWords(1234.56);
// → 'BİNİKİYÜZOTUZDÖRT TÜRK LİRASI ELLİALTI KURUŞ'

formatNumberToWords(1234.56, { lang: 'EN', currency: 'USD' });
// → 'ONE THOUSAND TWO HUNDRED THIRTY FOUR DOLAR FIFTY SIX CENT'

formatNumberToWords(1234.56, { lang: 'TR', currency: 'EUR', useSpaces: true });
// → 'BİN İKİ YÜZ OTUZ DÖRT EURO ELLİ ALTI CENT'

Options: | Option | Type | Default | Description | |--------|------|---------|-------------| | lang | 'TR' \| 'EN' | 'TR' | Word language | | currency | 'TRY' \| 'EUR' \| 'USD' \| 'GBP' \| 'JPY' | 'TRY' | Currency labels | | useSpaces | boolean | false | Add spaces between word groups |


Duration Formatters

Standalone functions for formatting durations (in seconds).

import {
  formatDurationAsTime,
  formatDurationAsHours,
  formatDurationAsWords
} from '@docyrus/app-utils';

formatDurationAsTime(5461);        // → '01:31'
formatDurationAsHours(5461);       // → '1.52'
formatDurationAsHours(5461, 1);    // → '1.5'
formatDurationAsWords(5461);       // → '1 hr 31 mins'
formatDurationAsWords(45);         // → '0 mins'

createTemplateEngine(config?)

Creates a template engine combining Handlebars (with async support) and JSONata formula evaluation.

import { createTemplateEngine } from '@docyrus/app-utils';

const engine = createTemplateEngine({
  dateUtils,                         // from createDateUtils()
  numberUtils,                       // from createNumberUtils()
  user: currentUser,                 // injected as metadata.user in formulas
  extraJsonataBindings: { hasRole }  // additional JSONata bindings
});

Each createTemplateEngine call creates an isolated Handlebars instance, so multiple engines (e.g., different tenants) won't conflict.

engine.compileTpl(templateString)

Preprocesses and compiles a Handlebars template. Returns an async function.

const tpl = engine.compileTpl('Hello {{name}}, total: {{formatNumber amount}}');
const html = await tpl({ name: 'John', amount: 1234.5 });
// → 'Hello John, total: 1.234,50'

Template preprocessing:

  1. {{formula $expr}} → wraps expression in quotes for the formula helper
  2. {{#if $expr}} → converts to {{#if (formula '$expr')}} for JSONata evaluation
  3. {{<content}} → strips HTML tags from inline content

engine.compileFormula(expression, data?)

Evaluates a JSONata expression with all helpers bound. Injects metadata.user from config.

const total = await engine.compileFormula('$sum(items.price)', { items });
const greeting = await engine.compileFormula('"Hello " & name', { name: 'World' });

engine.jsonataHelpers

Direct access to the full bindings object (all built-in helpers + extraJsonataBindings).


Handlebars Helpers

The following helpers are registered automatically when using compileTpl:

Formatting Helpers

| Helper | Usage | Description | |--------|-------|-------------| | formatDate | {{formatDate dateField format="d/m/Y"}} | Format date (uses tenant date_format by default) | | formatDateTime | {{formatDateTime dateField}} | Format datetime (uses tenant date_time_format) | | formatNumber | {{formatNumber amount decimalPrecision=2}} | Format number with tenant settings | | formatNumberToWords | {{formatNumberToWords amount lang="TR" currency="TRY"}} | Number to words | | formatDurationAsTime | {{formatDurationAsTime seconds}} | Duration as HH:MM | | formatDurationAsWords | {{formatDurationAsWords seconds}} | Duration as X hrs Y mins | | formatDurationAsHours | {{formatDurationAsHours seconds decimalPrecision=1}} | Duration as decimal hours |

Logic & Data Helpers

| Helper | Usage | Description | |--------|-------|-------------| | formula | {{formula "$sum(items.price)"}} or {{#formula}}$expr{{/formula}} | Evaluate JSONata expression | | path | {{#path "$.items[0]"}}...{{/path}} | JSONPath query — sets context to result | | repeat | {{#repeat 5}}...{{/repeat}} | Repeat block N times | | sum | {{sum items "price"}} or {{sum 1 2 3}} | Sum array field or values | | json | {{json data}} | Pretty-print JSON |


JSONata Helpers

All helpers are available inside compileFormula and {{formula}} blocks. They are also exported as jsonataHelpers for direct use.

import { jsonataHelpers } from '@docyrus/app-utils';

Date Functions (date-fns, null-safe wrapped)

All date functions accept strings or Date objects. Returns null for null/empty input.

Formatting: formatDate, formatDistance, formatDistanceStrict, formatDistanceToNow, formatDistanceToNowStrict, formatDuration, formatISO, formatRelative

Arithmetic: addYears, addMonths, addWeeks, addDays, addHours, addMinutes, addSeconds, addMilliseconds, subYears, subMonths, subWeeks, subDays, subHours, subMinutes, subSeconds, subMilliseconds

Comparison: isAfter, isBefore, isDatesEqual, isPast

Difference: differenceInYears, differenceInMonths, differenceInWeeks, differenceInDays, differenceInHours, differenceInMinutes, differenceInSeconds, differenceInBusinessDays, differenceInBusinessDaysCustom

Period boundaries: startOfDay, startOfMonth, startOfWeek, startOfToday, startOfQuarter, endOfDay, endOfMonth, endOfWeek, endOfToday, endOfQuarter

differenceInBusinessDaysCustom(startDate, endDate, options?)

Calculates business days with configurable work hours and lunch breaks.

$differenceInBusinessDaysCustom(startDate, endDate, {
  "workStartHour": 9,
  "workEndHour": 18,
  "lunchBreakHours": 1,
  "lunchStartHour": 12.5,
  "lunchEndHour": 13.5,
  "includePartialDays": true
})

Number / Format Functions

| Function | Signature | Description | |----------|-----------|-------------| | formatDecimal | (number, precision?, decSep?, thousandSep?) | Format decimal with separators | | formatMoney | (number, precision?, decSep?, thousandSep?, currency?, position?) | Format as currency | | percentage | (value, total, decimals) | Calculate percentage | | truncate | (text, limit) | Truncate with ... | | join | (array, separator) | Join array to string | | formatDurationInSeconds | (seconds) | Human-readable duration via date-fns |

String Functions

| Function | Signature | Description | |----------|-----------|-------------| | sha256 | (message) | SHA-256 hash (async) | | ascii | (str) | Strip accents and non-ASCII characters | | slug | (str, separator?) | URL-friendly slug | | extractNameFromEmail | (email, fallback?) | Extract name from email (e.g. john.doe@...John Doe) | | filterEmpty | (array) | Remove null/undefined/empty from array | | padLeft | (value, length, char?) | Left-pad string | | padRight | (value, length, char?) | Right-pad string | | ifNull | (value, alternative) | Null coalescing | | objectToString | (object) | Convert object to key:value string | | startsWith | (haystack, needle) | String starts with | | endsWith | (haystack, needle) | String ends with | | includes | (haystack, needle) | String includes (null-safe) |

Equality / DB Helpers

| Function | Signature | Description | |----------|-----------|-------------| | isEqual | (a, b) | Deep equality — compares by id, Date, JSON, or strict | | isEqualOrContained | (needle, haystack) | Check if value is equal to or contained in array | | isNotEqualOrContained | (needle, haystack) | Negation of isEqualOrContained | | getDbValue | (value) | Extract id from object or first item of array |

TypeScript

All types are exported:

import type {
  TenantPreferences,
  DateUtils,
  DateUtilsConfig,
  FormatOptions,
  NumberUtils,
  NumberUtilsConfig,
  NumberFormatOptions,
  NumberToWordsOptions,
  NumberWordLang,
  CurrencyCode,
  TemplateEngine,
  TemplateEngineConfig,
  AppConfig,
  UpsertAppConfigBody,
  AppConfigClient,
  UserAppConfig,
  UpsertUserAppConfigBody,
  UserAppConfigClient,
  DataView,
  CreateDataViewBody,
  UpdateDataViewBody,
  ListDataViewsParams,
  DataViewClient,
  DataForm,
  CreateDataFormBody,
  UpdateDataFormBody,
  DataFormClient,
  DataSourceExpand,
  DataSourceField,
  DataSource,
  DataSourceApp,
  DataSourceListParams,
  DataSourceClient,
  AppUtilsConfig
} from '@docyrus/app-utils';

License

MIT