{{breadcrumbs}}
← BACK TO HUB
HUB / bejson_core.ts

bejson_core.ts

Runtime
TypeScript
Category
TypeScript
Path
/storage/emulated/0/Projects/Management/Libraries/ts/bejson_core.ts
FILE // bejson_core.ts
/*
Library:     bejson_core.ts
MFDB Version: 1.3.1
Format_Creator: Elton Boehnen
Status:      OFFICIAL - v1.3.1
Date:        2026-05-06
*/

/**
 * Library:     bejson_core.ts
 * Jurisdiction: ["TYPESCRIPT", "CORE_COMMAND"]
 * Status:      OFFICIAL — Core-Command/Lib (v1.1)
 * Author:      Elton Boehnen
 * Version:     1.1 (OFFICIAL)
 * Date:        2026-04-23
 * Description: Core-Command library component.
 */
import {
  BEJSONDocument,
  BEJSONField,
  BEJSONValue,
  BEJSONCoreError,
  BEJSON_CORE_CODES,
} from "./bejson_types";

// ---------------------------------------------------------------------------
// Parse & Serialize
// ---------------------------------------------------------------------------


export function parse(json: string): BEJSONDocument {
  let raw: unknown;
  try {
    raw = JSON.parse(json);
  } catch (e) {
    throw new BEJSONCoreError(
      BEJSON_CORE_CODES.PARSE_ERROR,
      "Invalid JSON: " + String(e)
    );
  }
  if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
    throw new BEJSONCoreError(
      BEJSON_CORE_CODES.PARSE_ERROR,
      "Parsed JSON root must be an object."
    );
  }
  return raw as BEJSONDocument;
}


export function serialize(doc: BEJSONDocument, indent: number = 2): string {
  if (doc === null || doc === undefined) {
    throw new BEJSONCoreError(
      BEJSON_CORE_CODES.NULL_DOCUMENT,
      "Cannot serialize null or undefined document."
    );
  }
  try {
    return JSON.stringify(doc, null, indent || undefined);
  } catch (e) {
    throw new BEJSONCoreError(
      BEJSON_CORE_CODES.SERIALIZATION_ERROR,
      "Serialization failed: " + String(e)
    );
  }
}

// ---------------------------------------------------------------------------
// Field index helpers
// ---------------------------------------------------------------------------


export function getFieldIndex(doc: BEJSONDocument, name: string): number {
  _assertDoc(doc);
  const idx = doc.Fields.findIndex((f) => f.name === name);
  if (idx === -1) {
    throw new BEJSONCoreError(
      BEJSON_CORE_CODES.FIELD_NOT_FOUND,
      "Field not found: " + name
    );
  }
  return idx;
}


export function getFieldNames(doc: BEJSONDocument): string[] {
  _assertDoc(doc);
  return doc.Fields.map((f) => f.name);
}


export function getFields(doc: BEJSONDocument): BEJSONField[] {
  _assertDoc(doc);
  return doc.Fields.map((f) => Object.assign({}, f));
}

// ---------------------------------------------------------------------------
// Record accessors
// ---------------------------------------------------------------------------


export function getRecord(
  doc: BEJSONDocument,
  index: number
): Record<string, BEJSONValue> {
  _assertDoc(doc);
  _assertIndex(doc, index);
  return _rowToObject(doc.Fields, doc.Values[index]);
}


export function getAllRecords(
  doc: BEJSONDocument
): Record<string, BEJSONValue>[] {
  _assertDoc(doc);
  return doc.Values.map((row) => _rowToObject(doc.Fields, row));
}


export function getFieldValue(
  doc: BEJSONDocument,
  index: number,
  fieldName: string
): BEJSONValue {
  _assertDoc(doc);
  _assertIndex(doc, index);
  const fi = getFieldIndex(doc, fieldName);
  return doc.Values[index][fi];
}


export function getRecordCount(doc: BEJSONDocument): number {
  _assertDoc(doc);
  return doc.Values.length;
}

// ---------------------------------------------------------------------------
// 104db — entity-scoped record access
// ---------------------------------------------------------------------------


export function getRecordsByType(
  doc: BEJSONDocument,
  type: string
): Record<string, BEJSONValue>[] {
  _assertDoc(doc);
  if (doc.Format_Version !== "104db") {
    throw new BEJSONCoreError(
      BEJSON_CORE_CODES.UNSUPPORTED_OPERATION,
      "getRecordsByType is only valid on BEJSON 104db documents."
    );
  }
  // In 104db, the first field MUST be Record_Type_Parent (index 0)
  return doc.Values.filter((row) => row[0] === type).map((row) =>
    _rowToObject(doc.Fields, row)
  );
}


export function getFieldApplicability(
  doc: BEJSONDocument,
  fieldName: string
): string {
  _assertDoc(doc);
  const field = doc.Fields.find((f) => f.name === fieldName);
  if (!field) {
    throw new BEJSONCoreError(
      BEJSON_CORE_CODES.FIELD_NOT_FOUND,
      `Field not found: ${fieldName}`
    );
  }

  const rtp = field.Record_Type_Parent;
  if (doc.Format_Version === "104db") {
    if (!rtp) {
      if ((field as any).applies_to) {
        throw new BEJSONCoreError(
          BEJSON_CORE_CODES.UNSUPPORTED_OPERATION,
          `Field '${fieldName}' uses legacy 'applies_to'. 104db requires 'Record_Type_Parent'.`
        );
      }
      throw new BEJSONCoreError(
        BEJSON_CORE_CODES.UNSUPPORTED_OPERATION,
        `Field '${fieldName}' missing Record_Type_Parent in 104db`
      );
    }
  }
  return rtp || "common";
}


export function queryRecords(
  doc: BEJSONDocument,
  fieldName: string,
  searchValue: BEJSONValue
): Record<string, BEJSONValue>[] {
  _assertDoc(doc);
  const idx = getFieldIndex(doc, fieldName);
  return doc.Values.filter((row) => row[idx] === searchValue).map((row) =>
    _rowToObject(doc.Fields, row)
  );
}


export function sortByField(
  doc: BEJSONDocument,
  fieldName: string,
  ascending: boolean = true
): BEJSONDocument {
  _assertDoc(doc);
  const idx = getFieldIndex(doc, fieldName);
  const sortedValues = [...doc.Values].sort((a, b) => {
    const valA = a[idx];
    const valB = b[idx];
    if (valA === valB) return 0;
    if (valA === null) return 1;
    if (valB === null) return -1;
    
    let comparison = 0;
    if (typeof valA === 'string' && typeof valB === 'string') {
      comparison = valA.localeCompare(valB);
    } else {
      comparison = (valA as any) < (valB as any) ? -1 : 1;
    }
    return ascending ? comparison : -comparison;
  });

  return _cloneWith(doc, { Values: sortedValues });
}


export function getEntityFields(
  doc: BEJSONDocument,
  entityName: string
): BEJSONField[] {
  _assertDoc(doc);
  if (doc.Format_Version !== "104db") {
    throw new BEJSONCoreError(
      BEJSON_CORE_CODES.UNSUPPORTED_OPERATION,
      "getEntityFields is only valid on BEJSON 104db documents."
    );
  }
  return doc.Fields.filter(
    (f) => f.name !== "Record_Type_Parent" && f.Record_Type_Parent === entityName
  );
}

// ---------------------------------------------------------------------------
// Record mutations (return new document — documents are treated as immutable)
// ---------------------------------------------------------------------------


export function appendRecord(
  doc: BEJSONDocument,
  values: BEJSONValue[]
): BEJSONDocument {
  _assertDoc(doc);
  _assertRowLength(doc, values);
  const coerced = values.map((v, i) => _coerceValue(v, doc.Fields[i].type));
  return _cloneWith(doc, { Values: [...doc.Values, coerced] });
}


export function updateRecord(
  doc: BEJSONDocument,
  index: number,
  values: BEJSONValue[]
): BEJSONDocument {
  _assertDoc(doc);
  _assertIndex(doc, index);
  _assertRowLength(doc, values);
  const coerced = values.map((v, i) => _coerceValue(v, doc.Fields[i].type));
  const newValues = doc.Values.map((row, i) =>
    i === index ? coerced : row
  );
  return _cloneWith(doc, { Values: newValues });
}


export function setFieldValue(
  doc: BEJSONDocument,
  index: number,
  fieldName: string,
  value: BEJSONValue
): BEJSONDocument {
  _assertDoc(doc);
  _assertIndex(doc, index);
  const fi = getFieldIndex(doc, fieldName);
  const coerced = _coerceValue(value, doc.Fields[fi].type);
  const newValues = doc.Values.map((row, i) => {
    if (i !== index) return row;
    const newRow = [...row];
    newRow[fi] = coerced;
    return newRow;
  });
  return _cloneWith(doc, { Values: newValues });
}


export function deleteRecord(
  doc: BEJSONDocument,
  index: number
): BEJSONDocument {
  _assertDoc(doc);
  _assertIndex(doc, index);
  const newValues = doc.Values.filter((_, i) => i !== index);
  return _cloneWith(doc, { Values: newValues });
}

// ---------------------------------------------------------------------------
// Schema mutations
// ---------------------------------------------------------------------------


export function appendField(
  doc: BEJSONDocument,
  field: BEJSONField,
  defaultValue: BEJSONValue = null
): BEJSONDocument {
  _assertDoc(doc);
  if (doc.Fields.some((f) => f.name === field.name)) {
    throw new BEJSONCoreError(
      BEJSON_CORE_CODES.WRITE_LENGTH_MISMATCH,
      "Field already exists: " + field.name
    );
  }
  const newFields = [...doc.Fields, { ...field }];
  const newValues = doc.Values.map((row) => [...row, defaultValue]);
  return _cloneWith(doc, { Fields: newFields, Values: newValues });
}

// ---------------------------------------------------------------------------
// Document factory helpers
// ---------------------------------------------------------------------------


export function createEmpty104(
  recordType: string,
  fields: BEJSONField[],
  parentHierarchy?: string
): BEJSONDocument {
  const doc: BEJSONDocument = {
    Format: "BEJSON",
    Format_Version: "104",
    Format_Creator: "Elton Boehnen",
    Records_Type: [recordType],
    Fields: fields.map((f) => ({ ...f })),
    Values: [],
  };
  if (parentHierarchy !== undefined) {
    doc.Parent_Hierarchy = parentHierarchy;
  }
  return doc;
}


export function createEmpty104a(
  recordType: string,
  fields: BEJSONField[],
  customHeaders: Record<string, string | number | boolean> = {}
): BEJSONDocument {
  const doc: BEJSONDocument = {
    Format: "BEJSON",
    Format_Version: "104a",
    Format_Creator: "Elton Boehnen",
    ...customHeaders,
    Records_Type: [recordType],
    Fields: fields.map((f) => ({ ...f })),
    Values: [],
  };
  return doc;
}


export function createEmpty104db(
  recordTypes: string[],
  entityFields: BEJSONField[]
): BEJSONDocument {
  const discriminator: BEJSONField = {
    name: "Record_Type_Parent",
    type: "string",
  };
  return {
    Format: "BEJSON",
    Format_Version: "104db",
    Format_Creator: "Elton Boehnen",
    Records_Type: [...recordTypes],
    Fields: [discriminator, ...entityFields.map((f) => ({ ...f }))],
    Values: [],
  };
}

// ---------------------------------------------------------------------------
// Utility — flatten a record for 104db (strip discriminator, nulled fields)
// ---------------------------------------------------------------------------


export function flattenEntityRecord(
  doc: BEJSONDocument,
  record: Record<string, BEJSONValue>
): Record<string, BEJSONValue> {
  if (doc.Format_Version !== "104db") {
    throw new BEJSONCoreError(
      BEJSON_CORE_CODES.UNSUPPORTED_OPERATION,
      "flattenEntityRecord is only valid on BEJSON 104db documents."
    );
  }
  const entityName = record["Record_Type_Parent"] as string;
  const result: Record<string, BEJSONValue> = {};
  for (const field of doc.Fields) {
    if (field.name === "Record_Type_Parent") continue;
    if (field.Record_Type_Parent === entityName) {
      result[field.name] = record[field.name];
    }
  }
  return result;
}

// ---------------------------------------------------------------------------
// Internal helpers
// ---------------------------------------------------------------------------

function _assertDoc(doc: BEJSONDocument): void {
  if (doc === null || doc === undefined) {
    throw new BEJSONCoreError(
      BEJSON_CORE_CODES.NULL_DOCUMENT,
      "Document is null or undefined."
    );
  }
}

function _assertIndex(doc: BEJSONDocument, index: number): void {
  if (index < 0 || index >= doc.Values.length) {
    throw new BEJSONCoreError(
      BEJSON_CORE_CODES.INDEX_OUT_OF_BOUNDS,
      "Record index " + index + " is out of bounds (length " + doc.Values.length + ")."
    );
  }
}

function _assertRowLength(doc: BEJSONDocument, values: BEJSONValue[]): void {
  if (values.length !== doc.Fields.length) {
    throw new BEJSONCoreError(
      BEJSON_CORE_CODES.WRITE_LENGTH_MISMATCH,
      "Row length " + values.length + " does not match Fields length " + doc.Fields.length + "."
    );
  }
}

function _rowToObject(
  fields: BEJSONField[],
  row: BEJSONValue[]
): Record<string, BEJSONValue> {
  const obj: Record<string, BEJSONValue> = {};
  for (let i = 0; i < fields.length; i++) {
    obj[fields[i].name] = row[i];
  }
  return obj;
}

function _cloneWith(
  doc: BEJSONDocument,
  overrides: Partial<BEJSONDocument>
): BEJSONDocument {
  return Object.assign({}, doc, overrides);
}

function _coerceValue(value: any, fieldType: string): BEJSONValue {
  if (fieldType === "string") return String(value);
  if (fieldType === "integer" || fieldType === "number") {
    const num =
      fieldType === "integer" ? parseInt(value, 10) : parseFloat(value);
    if (isNaN(num)) {
      throw new BEJSONCoreError(
        BEJSON_CORE_CODES.WRITE_TYPE_MISMATCH,
        `Cannot convert '${value}' to ${fieldType}`
      );
    }
    return num;
  }
  if (fieldType === "boolean") {
    if (typeof value === "boolean") return value;
    if (typeof value === "string") {
      if (value.toLowerCase() === "true") return true;
      if (value.toLowerCase() === "false") return false;
    }
    throw new BEJSONCoreError(
      BEJSON_CORE_CODES.WRITE_TYPE_MISMATCH,
      `Cannot convert '${value}' to boolean`
    );
  }
  return value as BEJSONValue;
}