JSON Model-to-Model Projection (JM2MP) Documentation

JSON Model-to-Model Projection (JM2MP) Documentation

source

adapters/jsonquery.js

The module [JSONQuery]{@link module:jm2mp/adapters/jsonquery} implements the [QueryAdapter]{@link module:jm2mp/adapters/registry.QueryAdapter} interface to use the external [JSONQuery]{@link external:JSONQuery} query language as part of JM2MP projection documents.

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 [JSONQuery]{@link module:jm2mp/adapters/jsonquery} implements
 * the [QueryAdapter]{@link module:jm2mp/adapters/registry.QueryAdapter}
 * interface to use the external [JSONQuery]{@link external:JSONQuery}
 * _query language_ as part of `JM2MP` _projection documents_.
**/

/**
 * @module jm2mp/adapters/jsonquery
 * @description
 * This module implements
 * the [QueryAdapter]{@link module:jm2mp/adapters/registry.QueryAdapter}
 * interface to use the [JSON Query](https://jsonquerylang.org/) _query language_
 * as part of `JM2MP` _projection documents_.
 *
 * This module _only_ supports
 * [@jsonquerylang/jsonquery 4.x](https://www.npmjs.com/package/@jsonquerylang/jsonquery)
 * _version_.
 *
 * The `JSON Query` _query language_ supports two syntaxes:
 * - [String](https://jsonquerylang.org/docs/#text-format):
 *   textual representation with specific syntax for built-in functions composition.
 * - [Array](https://jsonquerylang.org/docs/#json-format):
 *   structured list of JSON values composing function calls used as query.
 *
 * This [JSONQuery]{@link external:JSONQuery}'s
 * [QueryAdapter]{@link module:jm2mp/adapters/registry.QueryAdapter}
 * accepts both syntaxes in `$path` to navigate/locate any JSON value
 * from _source documents_.
 *
 * By compliance with `JM2MP`, the
 * [QueryAdapter]{@link module:jm2mp/adapters/registry.QueryAdapter}
 * created by
 * [createJsonQueryAdapter]{@link module:jm2mp/adapters/jsonquery.createJsonQueryAdapter}
 * maintains the expected behaviour:
 * - `undefined`: returns `null` (absorption).
 * - Any runtime error found: raises
 *   [EvaluationError]{@link module:jm2mp/errors.EvaluationError}
 *    wrapping the original `cause`.
 * - `null` input: returns `null` without running
 *   [JSONQuery]{@link external:JSONQuery} library.
**/

/**
 * @external JSONQuery
 * @description
 * **JSON Query**: a small, flexible, and expandable JSON query language.
 * @see {@link https://jsonquerylang.org/}
 * @see {@link https://www.npmjs.com/package/@jsonquerylang/jsonquery}
 * @see {@link https://github.com/jsonquerylang/jsonquery}
**/

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

import { AdapterError, ValidationError, EvaluationError } from "../errors.js";

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

/**
 * @description
 * It creates a new
 * [QueryAdapter]{@link module:jm2mp/adapters/registry.QueryAdapter}
 * dynamically loading
 * [@jsonquerylang/jsonquery](https://www.npmjs.com/package/@jsonquerylang/jsonquery) version **4.x**.
 * @returns {Promise<module:jm2mp/adapters/registry.QueryAdapter>}
**/
export async function createJsonQueryAdapter()
{
  let jsonquery, parse;
  try {
    const mod = await import("@jsonquerylang/jsonquery");
    jsonquery = mod.jsonquery ?? mod.default?.jsonquery ?? mod.default;
    parse = mod.parse ?? mod.default?.parse;
    if ( ((typeof jsonquery) !== "function") &&
         ((typeof parse) !== "function") ) {
      throw new Error("Functions 'jsonquery' and 'parse' must be part of 'jsonquery' library.");
    }
  } catch (cause) {
    throw new AdapterError(
      "Unable to load '@jsonquerylang/jsonquery 4.x' external library. " +
      "To install it, please use: `npm install @jsonquerylang/jsonquery@4` .",
      { cause }
    );
  }

  /**
   * @constant
   * @type {module:jm2mp/adapters/registry.QueryAdapter}
   * @description
   * The newly created
   * [QueryAdapter]{@link module:jm2mp/adapters/registry.QueryAdapter}
   * for the [JSONQuery]{@link external:JSONQuery} query language.
  **/
  const new_jsonquerylang_query_adapter = {
    name:
      "jsonquery",
    description:
      "JSON Query 4.x query adapter.",

    /**
     * @description
     * It validates a JSON Query expression:
     * - If it is a `string`, it tries to `parse` it.
     * - It it is an `array` or `object`, it will a
     * - If it is an `array` or an `object` (it takes on a structured form),
     *   the actual validation will be deferred until runtime.
     * 
     * @param {string} path The JSON Query path to validate.
    **/
    async validate(path)
    {
      if ((typeof path) === "string")
      {
        if (path.length === 0)
        {
          throw new ValidationError(
            "JSON Query: $path must be a non-empty string."
          );
        }
        try
        {
          parse(path);
        }
        catch (cause)
        {
          throw new ValidationError(
            `Invalid JSON Query expression: "${path}".`,
            { cause }
          );
        }
      }
      else if ( Array.isArray(path) ||
                ( ((typeof path) === "object") && (path !== null) ) )
      {
        // If it is an `array` or an `object` (it takes on a structured form),
        // the actual validation will be deferred until runtime.
      }
      else
      {
        throw new ValidationError(
          `JSON Query: $path must be textual (a non-empty string) or structured (an array or an object), instead of '${(typeof path)}'.`
        );
      }
    },

    /**
     * @description
     * It evaluates a `JSON Query` expression, trying to standardize the
     * outcome in accordance with the interface
     * [QueryAdapter]{@link module:jm2mp/adapters/registry.QueryAdapter}.
     *
     * The `JSONQuery` library operates synchronously; `JM2MP.JS` wraps
     * the signature in `async` to comply with the uniform registry
     * contract (just like the rest of adapters).
     *
     * @param {*} path
     * The JSON Query path.
     * @param {*} input
     * The input used instead of execution environment.
     * @param {*} cache
     * It caches parsed query only when `$path` is a `string`.
     * @param {*} _env
     * The `env` parameter is ignored: `JSONQuery` does not support
     * lexical _aliases_ and does not distinguish between roots and
     * contexts, other than just the input; so the expression is always
     * evaluated against `input`.
    **/
    /* eslint-disable-next-line no-unused-vars -- _env */
    async evaluate(path, input, cache, _env) {
      // Null absorption propagation.
      if (input === null || input === undefined) return null;
      // It only caches string-formated parsed expressions.
      let queryToRun = path;
      if (typeof path === "string" && typeof parse === "function") {
        if (cache.has(path)) {
          queryToRun = cache.get(path);
        } else {
          try {
            queryToRun = parse(path);
          } catch (cause) {
            throw new EvaluationError(
              `Error during JSON Query expressions evaluation for "${path}".`,
              { cause }
            );
          }
          cache.set(path, queryToRun);
        }
      }

      let result;
      try {
        result = jsonquery(input, queryToRun);
      } catch (cause) {
        throw new EvaluationError(
          `Error evaluating JSON Query expression "${path}".`,
          { cause }
        );
      }

      // Null absorption propagation.
      return ((result === undefined) ? null : result);
    },

    /**
     * @property {@link module:jm2mp/adapters/registry.FallbackPolicyObject}
     * @description
     * [QueryAdapter]{@link module:jm2mp/adapters/registry.QueryAdapter}
     * behavior policy for edge cases in [JSONQuery]{@link external:JSONQuery}.
    **/
    fallbackPolicy: {
      missing:
        "null (when JSON Query return undefined)",
      multipleMatches:
        "array (JSON Query preserves the query format)",
      singleMatch:
        "scalar (as-is)",
      typeError:
        "EvaluationError (wrapping errors)",
      nullInput:
        "null (without invoking external library)",
      timeout:
        "0 (not async)"
    },
  };

  return new_jsonquerylang_query_adapter;

}  // export async function createJsonQueryAdapter

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