JSON Model-to-Model Projection (JM2MP) Documentation

JSON Model-to-Model Projection (JM2MP) Documentation

source

index.js

The module [index]{@link module:jm2mp/index} implements the high level API process of the projection language JM2MP.

Author

  • Luis Maria CAMARA ROSSI

Copyright

  • Universidad Nacional de Educación a Distancia (U.N.E.D.) 2026

License

  • BSD-3-Clause
/**
 * @author Luis Maria CAMARA ROSSI
 * @copyright Universidad Nacional de Educación a Distancia (U.N.E.D.) 2026
 * @license BSD-3-Clause
 * @file
 * The module [index]{@link module:jm2mp/index} implements the
 * high level API process of the _projection language_ JM2MP.
**/

/**
 * @module jm2mp/index
 * @description
 * This module implements the **evaluation process** of the _projection
 * language_ `JM2MP` and exposes it as a public API.
 * 
 * It exports:
 *
 * - Hierarchy of errors.
 *
 * - Pipeline: resolve, normalizeModule, validateModule, evaluate.
 *
 * - Loaders: string, file, and URL.
 *
 * - Query adapters: interfaces/contracts, individual factories, and
 *   registry's factory.
 *
 * - High level functionality:
 *   [project]{@link module:jm2mp/index.project}
 *   links all stages.
 * 
 *   The `native`
 *   [QueryAdapter]{@link module:jm2mp/adapters/registry.QueryAdapter}
 *   is always available; the external references (`jsonpath`,
 *   `jsonata`, `jsonquery`, `jsonpointer`, and `jmespath`) are
 *   [dynamically loaded]{@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import}
 *   on demand.
**/

/* ------------------------------------------------------------------ */
/* ------------------------------------------------------------------ */

/***
 ** Exporting the hierarchy of errors.
***/
export {
  ProjectionError,
  ParseError,
  ResolutionError,
  ValidationError,
  EvaluationError,
  AdapterError,
} from "./errors.js";

/***
 ** Exporting the projection pipeline.
***/
export { resolve, DEFAULT_MAX_MODULES } from "./modules/resolver.js";
export { validateModule } from "./validator.js";
export { evaluate, DEFAULT_MAX_DEPTH } from "./evaluator.js";
export { normalizeModule } from "./modules/normalizer.js";

/***
 ** Exporting query adapters and its registry.
***/
export { AdapterRegistry } from "./adapters/registry.js";
//// export { createNativeRegistry } from './adapters/helpers.js';
export { createNativeAdapter } from "./adapters/native.js";
export { createJsonPathAdapter } from "./adapters/jsonpath.js";
export { createJsonataAdapter } from "./adapters/jsonata.js";
export { createJsonQueryAdapter } from "./adapters/jsonquery.js";
export { createJsonPointerAdapter } from "./adapters/jsonpointer.js";
export { createJmesPathAdapter } from "./adapters/jmespath.js";

/***
 ** Exports loaders for modularization.
***/
export {
  createStringLoader,
  createFileLoader,
  createUrlLoader,
} from "./modules/loaders.js";

/* ------------------------------------------------------------------ */
/* ------------------------------------------------------------------ */

/***
 ** Internal imports for high level API.
***/
import { resolve as _resolve } from "./modules/resolver.js";
import { validateModule as _validate } from "./validator.js";
import { evaluate as _evaluate } from "./evaluator.js";
import { AdapterRegistry } from "./adapters/registry.js";
import { createNativeAdapter } from "./adapters/native.js";

/* ------------------------------------------------------------------ */
/* ------------------------------------------------------------------ */

/**
 * @description
 * It creates an
 * [adapter registry]{@link module:jm2mp/adapters/registry.AdapterRegistry}
 * with the
 * [query adapters]{@link module:jm2mp/adapters/registry.QueryAdapter}
 * specified by the caller.
 * 
 * The `native` _query adapter_ is **always** included automatically;
 * it requires no external dependencies.
 * 
 * It combines two ways to add adapters:
 * 
 * 1. Adapters predefined by the library: these are enabled using
 *    Boolean flags in the `defaultAdaptersToLoad` parameter. Their
 *    libraries are dynamically imported when enabled, so the cost is
 *    zero if they are not used.
 * 
 * 2. Custom adapters: these are passed as rest arguments after the
 *    first parameter; each one can be a pre-constructed
 *    [QueryAdapter]{@link module:jm2mp/adapters/registry.QueryAdapter}
 *    instance object or a _factory function_ (synchronous or
 *    asynchronous) that returns an actual
 *    [QueryAdapter]{@link module:jm2mp/adapters/registry.QueryAdapter}
 *    instance.
 *
 * @example
 * // native only:
 * const r1 = await createAdapterRegistry();
 *
 * // Native + JSONata (with timeout):
 * const r2 = await createAdapterRegistry({
 *   jsonata: true,
 *   jsonataOptions: { timeout: 5000 }
 * });
 *
 * // Native + JSONPath + JSON Pointer + JMESPath:
 * const r3 = await createAdapterRegistry({
 *   jsonpath: true,
 *   jsonpointer: true,
 *   jmespath: true,
 * });
 *
 *  // Native + another customized adapter:
 * const myAdapter = await createAnotherAdapter();
 * const r4 = await createAdapterRegistry({}, myAdapter);
 *
 * @param {object} [defaultAdaptersToLoad]
 * @param {boolean} [defaultAdaptersToLoad.jsonpath=false]
 * It registers the intention to use the `JSONPath` _query language_
 * (and imports the `jsonpath-plus` package).
 * @param {boolean} [defaultAdaptersToLoad.jsonata=false]
 * It registers the intention to use the `JSONata` _query language_
 * (and imports the `jsonata` package).
 * @param {object}  [defaultAdaptersToLoad.jsonataOptions]
 * It configures the options for `createJsonataAdapter` (timeout, ...).
 * @param {boolean} [defaultAdaptersToLoad.jsonquery=false]
 * It registers the intention to use the `JSON Query` _query language_
 * (and imports the `@jsonquerylang/jsonquery` package).
 * @param {boolean} [defaultAdaptersToLoad.jsonpointer=false]
 * It registers the intention to use the `JSON Pointer / RFC 6901`
 * _query language_ (and imports the `json-pointer` package).
 * @param {boolean} [defaultAdaptersToLoad.jmespath=false]
 * It registers the intention to use the `JMESPath` _query language_
 * (and imports the `jmespath` package).
 * @param {...(QueryAdapter | Function)} otherAdaptersToLoad
 * It registers the intention to use additional
 * [QueryAdapter]{@link module:jm2mp/adapters/registry.QueryAdapter}s,
 * allowing both _factory functions_ or already created objects.
 * @returns {Promise<AdapterRegistry>}
 * It returns a new configure
 * [AdapterRegistry]{@link module:jm2mp/adapters/registry.AdapterRegistry}.
 * @throws {module:jm2mp/errors.AdapterError}
 * Whenever a package cannot be imported.
 */
export async function createAdapterRegistry(
                                            defaultAdaptersToLoad = {},
                                            ...otherAdaptersToLoad )
{
  // It creates the new registry for query adapters...
  const registry = new AdapterRegistry();
  // ...always considering the native syntax.
  registry.register(createNativeAdapter());

  // Predefined adapters: dynamic loading using import.
  // External package is only required if corresponding
  // flag is activated.
  if (defaultAdaptersToLoad.jsonpath)
  {
    const { createJsonPathAdapter } = await import("./adapters/jsonpath.js");
    registry.register(await createJsonPathAdapter());
  }
  if (defaultAdaptersToLoad.jsonata)
  {
    const { createJsonataAdapter } = await import("./adapters/jsonata.js");
    registry.register(
      await createJsonataAdapter(defaultAdaptersToLoad.jsonataOptions)
    );
  }
  if (defaultAdaptersToLoad.jsonquery)
  {
    const { createJsonQueryAdapter } = await import("./adapters/jsonquery.js");
    registry.register(await createJsonQueryAdapter());
  }
  if (defaultAdaptersToLoad.jsonpointer)
  {
    const { createJsonPointerAdapter } = await import("./adapters/jsonpointer.js");
    registry.register(await createJsonPointerAdapter());
  }
  if (defaultAdaptersToLoad.jmespath)
  {
    const { createJmesPathAdapter } = await import("./adapters/jmespath.js");
    registry.register(await createJmesPathAdapter());
  }

  // Other customized adapters (rest parameters).
  // It supports both: sync/async factory objects
  // and literal objects.
  for (const item of otherAdaptersToLoad)
  {
    const adapter = (
      ((typeof item) === "function")
      ? (await item())
      : item
    );
    registry.register(adapter);
  }

  // It returns the just created result.
  return registry;
}

/* ------------------------------------------------------------------ */

/**
 * @typedef {Function} AsyncLoaderFunction
 * @description
 * typedef {(name: string) => Promise<object>} AsyncLoaderFunction
**/

/* ------------------------------------------------------------------ */

/**
 * @description
 * The high level convenience function: resolves, validates, and
 * evaluates.
 * 
 * It chains the three stages together in a safe order.
 * 
 * This is the recommended approach for standard usage.
 *
 * @param {object} params
 * @param {string} params.rootName
 * The name for the root _projection module_.
 * @param {AsyncLoaderFunction} params.loader
 * The loader of _projection modules_.
 * @param {*} params.document
 * The _source JSON document_ to project (transform).
 * @param {AdapterRegistry} [params.registry]
 * Si se omite, se crea uno con sólo el adaptador nativo.
 * @param {object} [params.options]
 * @param {number} [params.options.maxDepth=1000]
 * Maximum logical depth (to avoid infinite recursion, for example).
 * @param {number} [params.options.maxModules=1000]
 * Maximum number of loaded _projection modules_.
 * @returns {Promise<*>}
 * The _resultant JSON document_ obtained.
 */
export async function project({ rootName, loader, document, registry, options = {} })
{
  // When no registry is specified, a native-only wull be created.
  const actualRegistry = ( registry ?? (await createAdapterRegistry()));
  // Stage 1/3: resolve and normalize modules.
  const resolvedModule = await _resolve(rootName, loader, {
    maxModules: options.maxModules,
  });
  // Stage 2/3: validate the final module.
  await _validate(resolvedModule, actualRegistry);
  // Stage 3/3: evaluate the projection over the source.
  return await _evaluate(resolvedModule, document, {
    registry: actualRegistry,
    maxDepth: options.maxDepth,
  });
}

/* ------------------------------------------------------------------ */
/* ------------------------------------------------------------------ */
/* End of file: ${JM2MP.JS}/src/index.js                              */