Internals & Real-World Recipes
Deep dive into the recursive normalization engine and practical integration patterns for production Node.js applications.
1. Pipeline Overview #
The transformation pipeline converts deeply nested, XML-serialized Tally data into a predictable JavaScript array of objects through 3 core phases:
- Data Root Discovery: Drills into
json?.ENVELOPE?.BODY?.DATA?.COLLECTION. If empty or missing, returns an empty array safely. - Single Record Normalization: If Tally returned a single object instead of an array, it automatically wraps it into a 1-element array.
- Recursive Tree Dispatching: The engine walks every key, array, and object deeply, unwrapping
#textand stripping@_TYPEat every level.
2. Collection Extraction & Array Normalization #
Tally XML parsers produce objects when a collection has only 1 child, but arrays when it has >1 children. This is the #1 cause of bugs when integrating with Tally.
src/v2/index.js automatically detects single-child collections and normalizes them:
// Normalize single-record collection to an Array
const originalKeys = Object.keys(collection);
if (originalKeys.length === 1 && !Array.isArray(collection[originalKeys[0]])) {
collection[originalKeys[0]] = [collection[originalKeys[0]]];
}
It then uses pullKey() to dynamically locate the primary record array without hardcoding entity names:
const [key, rows] = Object.entries(collection).find(
([key, value]) => !key.startsWith("@") && Array.isArray(value)
) ?? [];
3. Recursive Dispatcher & Guards #
The tree walker in src/v2/changeType/v1/ uses functional guard predicates to avoid runtime exceptions:
const traverse = ({ inData }) => {
if (isNullOrUndefined({ inData })) return inData;
if (isTallyType({ inData })) return alterLeaf({ inValue: inData });
if (isArray({ inData })) return forArray({ inDataAsArray: inData });
if (isObject({ inData })) return forObject({ inDataAsObject: inData });
return inData;
};
4. Leaf Unboxing (alterLeaf.js) #
When a node contains an @_TYPE property, alterLeaf inspects the type:
- Empty node handling: If
#textis missing (e.g.<ALLINVENTORYENTRIES.LIST TYPE="String"/>), returns an empty string"". - Primitive extraction: Unboxes
#textand casts based on type (String, Date, Logical, Number, Rate, Amount).
5. Deep Nested Collections #
Vouchers in Tally have multi-tier nested arrays, such as:
tally-clean-response recurses through every level. ALLINVENTORYENTRIES.LIST and all inner BATCHALLOCATIONS.LIST entries are completely unwrapped into standard JavaScript arrays of objects.
Production Recipes
Recipe 1: Vouchers with Nested Inventory Entries
import { vouchers } from "tally-to-xml-tdl";
import cleanTallyResponse from "tally-clean-response";
const raw = await vouchers.purchases.period("MyCompany", "1-Apr-2026", "30-Apr-2026");
const purchases = cleanTallyResponse(raw);
for (const vch of purchases) {
console.log(`Voucher #${vch.VOUCHERNUMBER} on ${vch.DATE}`);
if (Array.isArray(vch["ALLINVENTORYENTRIES.LIST"])) {
for (const item of vch["ALLINVENTORYENTRIES.LIST"]) {
console.log(` - ${item.STOCKITEMNAME} • ${item.RATE} • Amount: ${item.AMOUNT}`);
}
}
}
Recipe 2: Master Catalogs (Ledgers, Stock, Units)
import { masters } from "tally-to-xml-tdl";
import cleanTallyResponse from "tally-clean-response";
// Fetch Units of Measure
const rawUom = await masters.get("MyCompany", "uom");
const units = cleanTallyResponse(rawUom);
// Fetch Stock Items
const rawStock = await masters.get("MyCompany", "stockItems");
const stockItems = cleanTallyResponse(rawStock);
Recipe 3: Direct Fast-XML-Parser Pipeline
import { XMLParser } from "fast-xml-parser";
import cleanTallyResponse from "tally-clean-response";
const parser = new XMLParser({
ignoreAttributes: false,
attributeNamePrefix: "@_",
textNodeName: "#text"
});
const rawJson = parser.parse(rawXmlStringFromTally);
const records = cleanTallyResponse(rawJson);
Recipe 4: Express REST API Endpoint
import express from "express";
import { vouchers } from "tally-to-xml-tdl";
import cleanTallyResponse from "tally-clean-response";
const app = express();
app.get("/api/vouchers/purchases", async (req, res) => {
try {
const { company, fromDate, toDate } = req.query;
const raw = await vouchers.purchases.period(company, fromDate, toDate);
const data = cleanTallyResponse(raw);
res.json({ success: true, count: data.length, data });
} catch (err) {
res.status(500).json({ success: false, error: err.message });
}
});
app.listen(3000, () => console.log("API running on :3000"));
Recipe 5: Strongly Typed TypeScript
import cleanTallyResponse from "tally-clean-response";
interface Voucher {
DATE: string;
GUID: string;
VOUCHERTYPENAME: string;
VOUCHERNUMBER: number;
"ALLINVENTORYENTRIES.LIST"?: {
STOCKITEMNAME: string;
RATE: string;
AMOUNT: number;
}[];
"@_REMOTEID"?: string;
}
const vouchers: Voucher[] = cleanTallyResponse<Voucher>(rawXmlJson);