code-docs/utils/helpers.md
Core helper functions used throughout the Lowdefy framework.
Provides universal utilities for:
import { get, set, type, serializer } from '@lowdefy/helpers';
Deep object property access with dot-notation:
const user = {
profile: {
name: 'John',
emails: ['[email protected]'],
},
};
get(user, 'profile.name'); // 'John'
get(user, 'profile.emails.0'); // '[email protected]'
get(user, 'profile.age', { default: 0 }); // 0
get(user, 'profile', { copy: true }); // Deep copy
Options:
| Option | Type | Description |
|---|---|---|
default | any | Return if path not found |
copy | boolean | Return deep copy |
Deep object property assignment:
const state = {};
set(state, 'user.name', 'John');
// state = { user: { name: 'John' } }
set(state, 'items.0.id', 1);
// state = { ..., items: [{ id: 1 }] }
set(state, 'config', { a: 1 }, { merge: true });
// Merges instead of replacing
Options:
| Option | Type | Description |
|---|---|---|
merge | boolean | Merge objects instead of replace |
Security: Prevents prototype pollution (__proto__, constructor, prototype).
Remove nested properties:
const state = { user: { name: 'John', age: 30 } };
unset(state, 'user.age');
// state = { user: { name: 'John' } }
Merge multiple objects using lodash.merge:
const defaults = { theme: 'light', debug: false };
const overrides = { debug: true };
mergeObjects([defaults, overrides]);
// { theme: 'light', debug: true }
Remove specified keys from object:
omit({ a: 1, b: 2, c: 3 }, ['b', 'c']);
// { a: 1 }
Swap array elements:
swap([1, 2, 3, 4], [0, 2]);
// [3, 2, 1, 4]
Apply array index operations:
applyArrayIndices(['a', 'b', 'c'], [1]);
// Applies index transformations
Comprehensive type checking:
import { type } from '@lowdefy/helpers';
// Basic types
type.isArray([]); // true
type.isObject({}); // true
type.isString(''); // true
type.isNumber(42); // true
type.isBoolean(true); // true
type.isNull(null); // true
type.isUndefined(undefined); // true
type.isNone(null); // true (null or undefined)
// Complex types
type.isDate(new Date()); // true
type.isError(new Error()); // true
type.isFunction(() => {}); // true
type.isPromise(Promise.resolve()); // true
type.isRegExp(/pattern/); // true
// Special checks
type.isPrimitive('string'); // true
type.isInteger(42); // true
// Type enforcement
type.enforceType('array', value); // Returns [] if not array
type.enforceType('string', 123); // Returns '123'
| Method | Checks For |
|---|---|
isArray | Array |
isObject | Plain object |
isString | String |
isNumber | Number (not NaN) |
isBoolean | Boolean |
isNull | null |
isUndefined | undefined |
isNone | null or undefined |
isDate | Date object |
isError | Error object |
isFunction | Function |
isPromise | Promise |
isRegExp | RegExp |
isSymbol | Symbol |
isPrimitive | Primitive type |
isInteger | Integer |
Coerce value to specified type:
type.enforceType('string', 123); // '123'
type.enforceType('number', '42'); // 42
type.enforceType('array', null); // []
type.enforceType('object', null); // {}
type.enforceType('boolean', 'yes'); // true
Handle JSON serialization with special types. IMPORTANT: Always use the serializer instead of raw JSON.stringify/JSON.parse when working with Lowdefy config objects to preserve internal metadata.
import { serializer } from '@lowdefy/helpers';
const data = {
date: new Date(),
error: new Error('message'),
};
// Serialize to string
const json = serializer.serializeToString(data);
// Deserialize from string
const restored = serializer.deserializeFromString(json);
// Deep copy with special type handling
const copy = serializer.copy(data);
// Standard serialize (for transport)
const serialized = serializer.serialize(data);
const deserialized = serializer.deserialize(serialized);
| Method | Description |
|---|---|
serialize(data) | Convert to JSON-safe object |
deserialize(data) | Restore from JSON-safe object |
serializeToString(data) | Convert to JSON string |
deserializeFromString(str) | Parse JSON string |
copy(data, options) | Deep copy with type handling |
The serializer specially handles non-enumerable internal properties used throughout Lowdefy:
| Property | Description |
|---|---|
~r | Reference ID - tracks which file an object came from |
~k | Key map ID - links objects to their config location |
~l | Line number - tracks source line numbers in YAML files |
~arr | Array wrapper - preserves ~k, ~r, ~l on arrays through JSON round-trips |
These properties are:
Object.keys(), spread operators)serializer.copy() and serializer.serialize()serializer.deserialize()Array serialization: Arrays can carry ~k, ~r, and ~l metadata. Since JSON can't store non-enumerable properties on arrays, the serializer wraps them as { "~arr": [...items], "~k": "...", "~r": "...", "~l": ... } during serialization and unwraps them on deserialization. Servers import build artifacts through serializer.deserialize() (in lib/build/*.js) to restore these markers at runtime.
Why this matters:
// BAD - loses internal properties like ~l (line numbers)
const copy = JSON.parse(JSON.stringify(configObject));
// GOOD - preserves all internal properties
const copy = serializer.copy(configObject);
Using with custom revivers:
// Copy with custom processing while preserving internal properties
const processed = serializer.copy(data, {
reviver: (key, value) => {
if (key === 'date') return new Date(value);
return value;
},
});
Cache promise results:
const fetchUser = cachedPromises(async (id) => {
return await api.getUser(id);
});
// First call fetches
await fetchUser('123');
// Second call returns cached
await fetchUser('123');
Least-Recently-Used cache:
import { LRUCache } from '@lowdefy/helpers';
const cache = new LRUCache({ maxSize: 100 });
cache.set('key', 'value');
cache.get('key'); // 'value'
cache.has('key'); // true
cache.delete('key');
cache.clear();
Parse and format URL query strings:
import { urlQuery } from '@lowdefy/helpers';
// Parse
urlQuery.parse('page=1&filter=active');
// { page: '1', filter: 'active' }
// Format
urlQuery.format({ page: 1, filter: 'active' });
// 'page=1&filter=active'
Deterministic JSON stringification:
import { stableStringify } from '@lowdefy/helpers';
// Objects with same keys in different order
// produce identical output
stableStringify({ b: 2, a: 1 });
stableStringify({ a: 1, b: 2 });
// Both: '{"a":1,"b":2}'
Promise-based delay:
import { wait } from '@lowdefy/helpers';
await wait(1000); // Wait 1 second
lodash.merge (4.6.2)Extracts all properties from an error object for serialization. Captures non-enumerable properties (message, name, stack, cause) plus all enumerable properties. Returns a clean, JSON-serialisable object — no circular references, no class instances.
import { extractErrorProps } from '@lowdefy/helpers';
const props = extractErrorProps(someError);
// { message: '...', name: 'ConfigError', stack: '...', configKey: 'abc123', ... }
Cause chain handling: Recursively serializes error.cause when the cause is an Error instance. Guarded by:
seen Set shared across all recursive callscleanValue (see below)Error objects found in other enumerable properties (not just cause) are also recursively extracted with the same circular reference protection.
Deep cleaning via cleanValue: Plain objects, arrays, and non-Error causes on error properties are recursively cleaned before inclusion. This prevents JSON.stringify crashes when error properties contain circular structures (e.g., Axios error responses with Node.js ClientRequest/IncomingMessage cycles). The cleaning rules:
'[Circular]'_extractErrorProps'[Object: ClassName]''[Truncated]'The seen Set is shared between cleanValue and _extractErrorProps, so circular references between error cause chains and nested plain objects are tracked in one pass. objectDepth and causeDepth are independent counters — object nesting always gets 5 levels regardless of cause chain position.
Used by:
createNodeLogger — determines what appears in JSON logs~e replacer — captures error data for ~e markerThe serializer handles errors via the ~e marker, preserving the correct Lowdefy error class through JSON round-trips.
Replacer:
// Error → { '~e': extractErrorProps(error) }
if (type.isError(newValue)) {
return { '~e': extractErrorProps(newValue) };
}
Reviver (via propsToError):
// { '~e': data } → Object.create(ErrorClass.prototype) + assign props
function propsToError(data) {
const ErrorClass = lowdefyErrorTypes[data.name] || Error;
const error = Object.create(ErrorClass.prototype);
for (const [k, v] of Object.entries(data)) {
if (k === 'cause' && v !== null && typeof v === 'object' && v.message !== undefined) {
error[k] = propsToError(v); // Recursively reconstruct cause chain
} else {
error[k] = v;
}
}
return error;
}
The lowdefyErrorTypes map imports Lowdefy error classes directly from @lowdefy/errors — no registration, no config passing. Includes: ConfigError, LowdefyInternalError, PluginError, ServiceError, UserError.
Why Object.create instead of new: Avoids calling constructors, which would re-format messages (PluginError adds location suffix, ServiceError adds service prefix). Setting message as a plain property on the instance shadows Error.prototype.message.
See errors.md for the full error class hierarchy and error-tracing.md for the complete error flow.
| File | Purpose |
|---|---|
src/get.js | Deep property access |
src/set.js | Deep property assignment |
src/type.js | Type checking module |
src/serializer.js | Serialization with ~e, ~d, ~r, ~k, ~l, ~arr markers |
src/extractErrorProps.js | Error property extraction for serializer and pino |
src/mergeObjects.js | Object merging |
src/LRUCache.js | LRU cache implementation |