Back to README (Overview)
Architecture & Recipes Guide

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:

Raw JSON → Collection Extractor → Array Normalizer → Recursive Dispatcher → Pristine Array
  1. Data Root Discovery: Drills into json?.ENVELOPE?.BODY?.DATA?.COLLECTION. If empty or missing, returns an empty array safely.
  2. Single Record Normalization: If Tally returned a single object instead of an array, it automatically wraps it into a 1-element array.
  3. Recursive Tree Dispatching: The engine walks every key, array, and object deeply, unwrapping #text and stripping @_TYPE at 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:

javascript • src/v2/index.js
// 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:

javascript
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:

javascript • traverse.js
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 #text is missing (e.g. <ALLINVENTORYENTRIES.LIST TYPE="String"/>), returns an empty string "".
  • Primitive extraction: Unboxes #text and casts based on type (String, Date, Logical, Number, Rate, Amount).

5. Deep Nested Collections #

Vouchers in Tally have multi-tier nested arrays, such as:

VOUCHER
  └−− ALLINVENTORYENTRIES.LIST[]
    └−− STOCKITEMNAME
    └−− BATCHALLOCATIONS.LIST[]
      └−− BATCHNAME
      └−− AMOUNT

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

javascript
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)

javascript
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

javascript
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

javascript
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

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);