{{breadcrumbs}}
← BACK TO HUB
HUB / lib_mfdb_validator.js

lib_mfdb_validator.js

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

/**
 * Library:     lib_mfdb_validator.js
 * Jurisdiction: ["JAVASCRIPT", "CORE_COMMAND"]
 * Status:      OFFICIAL — Core-Command/Lib (v1.2)
 * Author:      Elton Boehnen
 * Version:     1.3 OFFICIAL
 * Date:        2026-05-01
 * Description: MFDB (Multifile Database) validation library.
 *              v1.2 adds support for validating .mfdb.zip archives.
 */
'use strict';

const {
  BEJSONValidationError,
  bejson_validator_validate_file,
  bejson_validator_validate_string,
} = (typeof require !== 'undefined')
  ? require('./lib_bejson_validator.js')
  : window.BEJSON_VALIDATOR;

// ---------------------------------------------------------------------------
// Error codes (30–49)
// ---------------------------------------------------------------------------

const E_MFDB_NOT_MANIFEST           = 30;
const E_MFDB_NOT_ENTITY_FILE        = 31;
const E_MFDB_MANIFEST_RECORDS_TYPE  = 32;
const E_MFDB_ENTITY_NOT_FOUND       = 33;
const E_MFDB_ENTITY_NAME_MISMATCH   = 34;
const E_MFDB_DUPLICATE_ENTRY        = 35;
const E_MFDB_NO_PARENT_HIERARCHY    = 36;
const E_MFDB_MANIFEST_NOT_FOUND     = 37;
const E_MFDB_BIDIRECTIONAL_FAIL     = 38;
const E_MFDB_FK_UNRESOLVED          = 39;
const E_MFDB_MISSING_REQUIRED_FIELD = 40;
const E_MFDB_NULL_REQUIRED          = 41;
const E_MFDB_INVALID_ARCHIVE        = 42;

class MFDBValidationError extends Error {
  constructor(message, code) {
    super(message);
    this.name = 'MFDBValidationError';
    this.code = code;
  }
}

// ---------------------------------------------------------------------------
// Validation state
// ---------------------------------------------------------------------------

let _mErrors   = [];
let _mWarnings = [];

function _mReset()                  { _mErrors = []; _mWarnings = []; }
function _mAddError(msg, loc)       { _mErrors.push(   'ERROR'   + (loc ? ` | Location: ${loc}` : '') + ` | Message: ${msg}`); }
function _mAddWarning(msg, loc)     { _mWarnings.push( 'WARNING' + (loc ? ` | Location: ${loc}` : '') + ` | Message: ${msg}`); }
function _mHasErrors()              { return _mErrors.length   > 0; }
function _mHasWarnings()            { return _mWarnings.length > 0; }


// ---------------------------------------------------------------------------
// Internal helpers (also exported for lib_mfdb_core.js)
// ---------------------------------------------------------------------------


function _loadJson(filePath) {
  const fs = require('fs');
  return JSON.parse(fs.readFileSync(filePath, 'utf8'));
}


function _rowsAsDicts(doc) {
  const names = doc.Fields.map(f => f.name);
  return doc.Values.map(row => Object.fromEntries(names.map((n, i) => [n, row[i]])));
}


function _resolveEntityPath(manifestPath, filePathRel) {
  const path = require('path');
  return path.resolve(path.dirname(manifestPath), filePathRel);
}


function _fileExists(filePath) {
  const fs = require('fs');
  return fs.existsSync(filePath);
}


// ---------------------------------------------------------------------------
// Archive Validation (v1.2 Feature)
// ---------------------------------------------------------------------------

function mfdb_validator_validate_archive(archivePath) {
  _mReset();
  const AdmZip = require('adm-zip');

  if (!_fileExists(archivePath)) {
    _mAddError(`Archive not found: ${archivePath}`, 'File System');
    throw new MFDBValidationError(`Archive not found: ${archivePath}`, E_MFDB_MANIFEST_NOT_FOUND);
  }

  try {
    const zip = new AdmZip(archivePath);
    const zipEntries = zip.getEntries();
    const hasManifest = zipEntries.some(e => e.entryName === '104a.mfdb.bejson');
    if (!hasManifest) {
      _mAddError("Archive missing 104a.mfdb.bejson at root", "Zip Structure");
      throw new MFDBValidationError("Missing manifest inside archive", E_MFDB_INVALID_ARCHIVE);
    }
  } catch (exc) {
    _mAddError(`Invalid zip file: ${exc.message}`, "Zip Parser");
    throw new MFDBValidationError(exc.message, E_MFDB_INVALID_ARCHIVE);
  }

  return true;
}


// ---------------------------------------------------------------------------
// Manifest Validation  (Spec §8.1)
// ---------------------------------------------------------------------------


function mfdb_validator_validate_manifest(manifestPath) {
  _mReset();

  if (!_fileExists(manifestPath)) {
    _mAddError(`Manifest file not found: ${manifestPath}`, 'File System');
    throw new MFDBValidationError(`File not found: ${manifestPath}`, E_MFDB_MANIFEST_NOT_FOUND);
  }

  try {
    bejson_validator_validate_file(manifestPath);
  } catch (exc) {
    _mAddError(`BEJSON 104a validation failed: ${exc.message}`, 'BEJSON Validation');
    throw new MFDBValidationError(exc.message, E_MFDB_NOT_MANIFEST);
  }

  const doc = _loadJson(manifestPath);

  if (doc.Format_Version !== '104a') {
    _mAddError("Manifest must be Format_Version '104a'", 'Format_Version');
    throw new MFDBValidationError('Manifest must be 104a', E_MFDB_NOT_MANIFEST);
  }

  const rt = doc.Records_Type || [];
  if (!(rt.length === 1 && rt[0] === 'mfdb')) {
    _mAddError(`Records_Type must be ["mfdb"]. Found: ${JSON.stringify(rt)}`, 'Records_Type');
    throw new MFDBValidationError('Bad manifest Records_Type', E_MFDB_MANIFEST_RECORDS_TYPE);
  }

  const fieldNames = (doc.Fields || []).map(f => f.name);
  for (const required of ['entity_name', 'file_path']) {
    if (!fieldNames.includes(required)) {
      _mAddError(`Manifest Fields must include '${required}'`, 'Fields');
      throw new MFDBValidationError(`Missing required field '${required}'`, E_MFDB_MISSING_REQUIRED_FIELD);
    }
  }

  const entries    = _rowsAsDicts(doc);
  const seenNames  = new Set();
  const seenPaths  = new Set();

  for (let i = 0; i < entries.length; i++) {
    const entry      = entries[i];
    const entityName = entry.entity_name;
    const filePath   = entry.file_path;

    if (!entityName) {
      _mAddError(`Record ${i}: entity_name is null or missing`, `Values[${i}]`);
      throw new MFDBValidationError('Null entity_name', E_MFDB_NULL_REQUIRED);
    }
    if (!filePath) {
      _mAddError(`Record ${i}: file_path is null or missing`, `Values[${i}]`);
      throw new MFDBValidationError('Null file_path', E_MFDB_NULL_REQUIRED);
    }
    if (seenNames.has(entityName)) {
      _mAddError(`Duplicate entity_name: '${entityName}'`, `Values[${i}]`);
      throw new MFDBValidationError(`Duplicate entity_name: ${entityName}`, E_MFDB_DUPLICATE_ENTRY);
    }
    seenNames.add(entityName);

    if (seenPaths.has(filePath)) {
      _mAddError(`Duplicate file_path: '${filePath}'`, `Values[${i}]`);
      throw new MFDBValidationError(`Duplicate file_path: ${filePath}`, E_MFDB_DUPLICATE_ENTRY);
    }
    seenPaths.add(filePath);

    const resolved = _resolveEntityPath(manifestPath, filePath);
    if (!_fileExists(resolved)) {
      _mAddError(`Entity file '${filePath}' not found (resolved: ${resolved})`, `Values[${i}]/file_path`);
      throw new MFDBValidationError(`Entity file not found: ${resolved}`, E_MFDB_ENTITY_NOT_FOUND);
    }
  }

  return true;
}


// ---------------------------------------------------------------------------
// Entity File Validation  (Spec §8.2)
// ---------------------------------------------------------------------------


function mfdb_validator_validate_entity_file(entityPath, checkBidirectional = true) {
  const path = require('path');
  _mReset();

  if (!_fileExists(entityPath)) {
    _mAddError(`Entity file not found: ${entityPath}`, 'File System');
    throw new MFDBValidationError(`File not found: ${entityPath}`, E_MFDB_ENTITY_NOT_FOUND);
  }

  try {
    bejson_validator_validate_file(entityPath);
  } catch (exc) {
    _mAddError(`BEJSON 104 validation failed: ${exc.message}`, 'BEJSON Validation');
    throw new MFDBValidationError(exc.message, E_MFDB_NOT_ENTITY_FILE);
  }

  const doc = _loadJson(entityPath);

  if (doc.Format_Version !== '104') {
    _mAddError("Entity file must be Format_Version '104'", 'Format_Version');
    throw new MFDBValidationError('Entity file must be 104', E_MFDB_NOT_ENTITY_FILE);
  }

  const parentHierarchy = doc.Parent_Hierarchy;
  if (!parentHierarchy) {
    _mAddError('Entity file must contain Parent_Hierarchy pointing to the manifest', 'Parent_Hierarchy');
    throw new MFDBValidationError('Missing Parent_Hierarchy', E_MFDB_NO_PARENT_HIERARCHY);
  }

  const entityDir    = path.dirname(path.resolve(entityPath));
  const manifestPath = path.resolve(entityDir, parentHierarchy);

  if (!_fileExists(manifestPath)) {
    _mAddError(
      `Parent_Hierarchy '${parentHierarchy}' resolves to '${manifestPath}' which does not exist`,
      'Parent_Hierarchy',
    );
    throw new MFDBValidationError(`Manifest not found: ${manifestPath}`, E_MFDB_MANIFEST_NOT_FOUND);
  }

  if (!path.basename(manifestPath).endsWith('.mfdb.bejson')) {
    _mAddWarning(
      `Parent_Hierarchy target '${manifestPath}' does not end in '.mfdb.bejson'. Expected: 104a.mfdb.bejson`,
      'Parent_Hierarchy',
    );
  }

  const rt = doc.Records_Type || [];
  if (rt.length !== 1) {
    _mAddError(`Entity file Records_Type must have exactly one entry. Found: ${JSON.stringify(rt)}`, 'Records_Type');
    throw new MFDBValidationError('Entity Records_Type must be single-entry', E_MFDB_NOT_ENTITY_FILE);
  }

  const entityName = rt[0];

  let manifestDoc, entries, manifestEntityNames;
  try {
    manifestDoc        = _loadJson(manifestPath);
    entries            = _rowsAsDicts(manifestDoc);
    manifestEntityNames = entries.map(e => e.entity_name);
  } catch (exc) {
    _mAddError(`Could not read manifest: ${exc.message}`, 'Manifest');
    throw new MFDBValidationError(`Cannot read manifest: ${exc.message}`, E_MFDB_MANIFEST_NOT_FOUND);
  }

  if (!manifestEntityNames.includes(entityName)) {
    _mAddError(
      `Records_Type '${entityName}' does not appear as entity_name in the manifest`,
      'Records_Type vs Manifest',
    );
    throw new MFDBValidationError(
      `Entity '${entityName}' not registered in manifest`, E_MFDB_ENTITY_NAME_MISMATCH,
    );
  }

  if (checkBidirectional) {
    const match = entries.find(e => e.entity_name === entityName);
    if (match) {
      const manifestDir    = path.dirname(path.resolve(manifestPath));
      const fromManifest   = path.resolve(manifestDir, match.file_path || '');
      const thisFile       = path.resolve(entityPath);
      if (fromManifest !== thisFile) {
        _mAddError(
          `Bidirectional check failed for entity '${entityName}': ` +
          `manifest points to '${fromManifest}', but this file is '${thisFile}'`,
          'Bidirectional Path Check',
        );
        throw new MFDBValidationError('Bidirectional path check failed', E_MFDB_BIDIRECTIONAL_FAIL);
      }
    }
  }

  return true;
}


// ---------------------------------------------------------------------------
// Database-Level Validation  (Spec §8.3)
// ---------------------------------------------------------------------------


function mfdb_validator_validate_database(manifestPath, strictFk = false) {
  _mReset();

  try {
    mfdb_validator_validate_manifest(manifestPath);
  } catch (exc) {
    throw exc;
  }

  const manifestDoc = _loadJson(manifestPath);
  const entries     = _rowsAsDicts(manifestDoc);

  const pkMap = {};
  for (const e of entries) {
    if (e.primary_key) pkMap[e.entity_name] = e.primary_key;
  }

  for (const entry of entries) {
    const entityName    = entry.entity_name;
    const filePathRel   = entry.file_path;
    const declaredCount = entry.record_count;

    const resolved = _resolveEntityPath(manifestPath, filePathRel);

    try {
      mfdb_validator_validate_entity_file(resolved, true);
    } catch (exc) {
      _mAddError(`Entity '${entityName}' failed validation: ${exc.message}`, `Entity/${entityName}`);
      throw exc;
    }

    if (declaredCount !== null && declaredCount !== undefined) {
      const edoc        = _loadJson(resolved);
      const actualCount = (edoc.Values || []).length;
      if (actualCount !== declaredCount) {
        _mAddWarning(
          `Entity '${entityName}': manifest declares record_count=${declaredCount}, ` +
          `actual=${actualCount}. Call mfdb_core_sync_all_counts() to correct.`,
          `Entity/${entityName}/record_count`,
        );
      }
    }

    if (strictFk) {
      const edoc    = _loadJson(resolved);
      const fkFields = (edoc.Fields || []).filter(f => f.name.endsWith('_fk')).map(f => f.name);
      for (const fkField of fkFields) {
        const targetFound = Object.entries(pkMap).some(
          ([en, pk]) => pk && (fkField.includes(pk) || fkField.toLowerCase().includes(en.toLowerCase()))
        );
        if (!targetFound) {
          _mAddWarning(
            `Entity '${entityName}': FK field '${fkField}' has no matching primary_key ` +
            `declaration in the manifest. Consider adding a Relationships header (MFDB v1.1).`,
            `Entity/${entityName}/${fkField}`,
          );
        }
      }
    }
  }

  return true;
}


// ---------------------------------------------------------------------------
// Validation report
// ---------------------------------------------------------------------------


function mfdb_validator_get_report(manifestPath, strictFk = false) {
  let valid = false;
  try {
    valid = mfdb_validator_validate_database(manifestPath, strictFk);
  } catch (_) {}

  const lines = [
    '=== MFDB Validation Report ===',
    `Manifest : ${manifestPath}`,
    `Status   : ${valid ? 'VALID' : 'INVALID'}`,
    '',
    `Errors   : ${_mErrors.length}`,
  ];
  if (_mHasErrors()) {
    lines.push('---');
    lines.push(..._mErrors);
  }
  lines.push('', `Warnings : ${_mWarnings.length}`);
  if (_mHasWarnings()) {
    lines.push('---');
    lines.push(..._mWarnings);
  }
  return lines.join('\n');
}


// ---------------------------------------------------------------------------
// State accessors
// ---------------------------------------------------------------------------

function mfdb_validator_reset_state()      { _mReset(); }
function mfdb_validator_has_errors()       { return _mHasErrors(); }
function mfdb_validator_has_warnings()     { return _mHasWarnings(); }
function mfdb_validator_get_errors()       { return [..._mErrors]; }
function mfdb_validator_get_warnings()     { return [..._mWarnings]; }
function mfdb_validator_error_count()      { return _mErrors.length; }
function mfdb_validator_warning_count()    { return _mWarnings.length; }


// ---------------------------------------------------------------------------
// Exports (CommonJS + browser global)
// ---------------------------------------------------------------------------

const exports_ = {
  // Error codes
  E_MFDB_NOT_MANIFEST,
  E_MFDB_NOT_ENTITY_FILE,
  E_MFDB_MANIFEST_RECORDS_TYPE,
  E_MFDB_ENTITY_NOT_FOUND,
  E_MFDB_ENTITY_NAME_MISMATCH,
  E_MFDB_DUPLICATE_ENTRY,
  E_MFDB_NO_PARENT_HIERARCHY,
  E_MFDB_MANIFEST_NOT_FOUND,
  E_MFDB_BIDIRECTIONAL_FAIL,
  E_MFDB_FK_UNRESOLVED,
  E_MFDB_MISSING_REQUIRED_FIELD,
  E_MFDB_NULL_REQUIRED,
  E_MFDB_INVALID_ARCHIVE,
  // Class
  MFDBValidationError,
  // Internal helpers (needed by lib_mfdb_core.js)
  _loadJson,
  _rowsAsDicts,
  _resolveEntityPath,
  _fileExists,
  // Validation functions
  mfdb_validator_validate_archive,
  mfdb_validator_validate_manifest,
  mfdb_validator_validate_entity_file,
  mfdb_validator_validate_database,
  mfdb_validator_get_report,
  // State accessors
  mfdb_validator_reset_state,
  mfdb_validator_has_errors,
  mfdb_validator_has_warnings,
  mfdb_validator_get_errors,
  mfdb_validator_get_warnings,
  mfdb_validator_error_count,
  mfdb_validator_warning_count,
};

if (typeof module !== 'undefined' && module.exports) {
  module.exports = exports_;
}
if (typeof window !== 'undefined') {
  window.MFDB_VALIDATOR = exports_;
}