db/database.js

import { matcher } from "../tag_utils.js";
import { random } from "../random_utils.js";

/**
 * A generic database class for storing and querying models. Subclasses can implement
 * more specific logic around each model (particularly, the code to convert
 * configuration data into specific models...each model type can have its own compact
 * serialization format).
 * @class Database
 */
class Database {
  /**
   * Creates a new Database instance.
   * @param {Object} [options={}] - Configuration options
   * @param {Function} [options.parser] - Parser function to process configuration data
   *    before adding it to the database. If not provided, configuration is passed
   *    directly to the model
   */
  constructor({ parser = (m) => m } = {}) {
    /**
     * Array of models stored in the database.
     * @type {Array}
     */
    this.models = [];

    /**
     * Parser function used to process data before passing it to the model constructor
     * @type {Function}
     */
    this.parser = parser;
  }

  /**
   * Adds one or more data items to the database.
   * @param {...*} data - Data items to add to the database
   * @returns {Database} Returns this instance for method chaining
   *
   * @example
   * database.add(item1, item2, item3);
   */
  add(...data) {
    for (let i = 0, len = data.length; i < len; i++) {
      var records = this.parser(data[i]);
      records.forEach((r) => (this.models[this.models.length] = r));
    }
    return this;
  }

  /**
   * Finds all models that match the specified criteria. Subclasses may add additional
   * search parameters, but all models support searching by tags.
   *
   * @param {Object} [options={}] - Search criteria
   * @param {string} [options.tags] - Tag filter string
   * @returns {Array} Array of matching models
   *
   * @example
   * database.findAll({ tags: "weapon firearm" });
   */
  findAll({ tags } = {}) {
    return this.models.filter((m) => tagFilter(m, tags));
  }

  /**
   * Finds a random model that matches the specified criteria. Subclasses may add
   * additional search parameters, but all models support searching by tags.
   *
   * @param {Object} [options={}] - Search criteria
   * @param {string} [options.tags] - Tag filter string
   * @returns {*} A random matching model, or undefined if none found
   *
   * @example
   * database.findOne({ tags: "weapon firearm" });
   */
  findOne({ tags } = {}) {
    return random(this.findAll({ tags }));
  }
}

export default Database;

/**
 * Filter helper functions for creating consistent sorting criteria in database subclasses.
 * These functions should be tested with && expressions between them for consistency
 * (all supplied criteria must match, but if nothing is supplied, no match is made).
 */

/**
 * Filters models by ID.
 * @function idFilter
 * @param {Object} m - The model to test
 * @param {*} id - The ID to match against (undefined means no filtering)
 * @returns {boolean} True if the model matches or no ID filter is specified
 *
 * @example
 * models.filter(m => idFilter(m, "item123"));
 */
export function idFilter(m, id) {
  return typeof id === "undefined" ? true : m.id === id;
}

/**
 * Filters models by type (case-insensitive).
 * @function typeFilter
 * @param {Object} m - The model to test
 * @param {string} type - The type to match against (undefined means no filtering)
 * @returns {boolean} True if the model matches or no type filter is specified
 *
 * @example
 * models.filter(m => typeFilter(m, "weapon"));
 */
export function typeFilter(m, type) {
  return typeof type === "undefined" ? true : m.type.toLowerCase() === type.toLowerCase();
}

/**
 * Filters models by tags using tag matching logic.
 * @function tagFilter
 * @param {Object} m - The model to test
 * @param {string} tags - The tag filter string (undefined means no filtering)
 * @returns {boolean} True if the model matches or no tag filter is specified
 *
 * @example
 * models.filter(m => tagFilter(m, "weapon firearm"));
 */
export function tagFilter(m, tags) {
  return typeof tags === "undefined" ? true : matcher(tags, m.tags);
}

/**
 * Filters models by checking if a name exists in the model's names array.
 * @function namesFilter
 * @param {Object} m - The model to test
 * @param {string} name - The name to search for (undefined means no filtering)
 * @returns {boolean} True if the model contains the name or no name filter is specified
 *
 * @example
 * models.filter(m => namesFilter(m, "John"));
 */
export function namesFilter(m, name) {
  return typeof name === "undefined" ? true : m.names.includes(name);
}