dodil/erp-ui

src/lib/json.ts

/**
 * JSON without losing ids.
 *
 * Bricks key partners by a 64-bit integer derived from a business key (`"bp_id": 45665172796086509`). JSON
 * has only doubles, and `JSON.parse` rounds anything past 2^53 to the nearest double — 45665172796086509
 * becomes 45665172796086510, a DIFFERENT partner, and the next request for it is a 404. Found building the CRM
 * screens (2026-10-17): every bp_id the API returns is past 2^53.
 *
 * `quoteUnsafeIntegers` rewrites the text so every integer a double cannot hold exactly arrives as a string;
 * everything else is untouched (strings are skipped, decimals and safe integers stay numbers). Use the
 * value with `String(x)`, and it is exact whichever way it arrived.
 */

export function quoteUnsafeIntegers(text: string): string {
  let out = "";
  let i = 0;
  const n = text.length;
  while (i < n) {
    const ch = text[i];
    if (ch === '"') {
      let j = i + 1;
      while (j < n && text[j] !== '"') j += text[j] === "\\" ? 2 : 1;
      out += text.slice(i, j + 1);
      i = j + 1;
    } else if (ch === "-" || (ch >= "0" && ch <= "9")) {
      let j = i + 1;
      while (j < n && /[0-9eE+\-.]/.test(text[j])) j++;
      const token = text.slice(i, j);
      out += /^-?\d+$/.test(token) && !Number.isSafeInteger(Number(token)) ? `"${token}"` : token;
      i = j;
    } else {
      out += ch;
      i++;
    }
  }
  return out;
}

/** `JSON.parse`, with integers past 2^53 kept exactly (as strings). */
export function parseJsonLossless<T = unknown>(text: string): T {
  return JSON.parse(quoteUnsafeIntegers(text)) as T;
}