errors.js

/**
 * <p>
 * This is the only file that is permitted to create new Error objects.
 * Any throw that occurs in this library will be one of the errors listed.
 * </p>
 *
 * <p>
 * As such, any try/catch code you write against this library should make
 * sure to not blind-catch, but catch intentionally:
 * <p>
 *
 * <pre><code>
 *   import { Errors } from "use-models-for-data";
 *   import { MyModel } from "./my-models.js";
 *
 *   try {
 *     MyModel.load(`someRecordName`);
 *   } catch (err) {
 *     const type = err.__proto__.constructor;
 *     switch(type) {
 *       case (Errors.RecordDoesNotExist): ...
 *       case (Errors.RecordAccessError): ...
 *       ...
 *       case (TypeError): ...
 *       default: throw err;
 *     }
 *   }
 * </code></pre>
 *
 *
 * @namespace Errors
 */

export class MissingCreateFunction extends Error {
  /**
   * Used by the form building code to warn the user that they forgot to provide a create() function for transforming tag/props tuples into nodes.
   * @name Errors.MissingCreateFunction
   * @member
   */
  constructor() {
    super();
    this.message = `No create(tag, props) function provided.`;
  }
}

export class NothingToMigrate extends Error {
  /**
   * Used in migration code to signal that there are no actual diffs to migrate, despite being in a codepath that assumes that there are.
   * @name Errors.NothingToMigrate
   * @member
   */
  constructor() {
    super();
    this.message = `Nothing to migrate.`;
  }
}

export class PropertySchemaViolation extends Error {
  /**
   * Used in schema validation code, specifically the array wrapper, to signal an illegal value assignment.
   * @name Errors.PropertySchemaViolation
   * @member
   */
  constructor(errors) {
    super();
    this.message = `Assignment violates property schema.`;
    this.errors = errors;
  }
}

export class MissingChoicesArray extends Error {
  /**
   * Used in the model fields code to signal that an expected <code>choices</code> array is missing from the field properties.
   * @name Errors.MissingChoicesArray
   * @member
   */
  constructor() {
    super();
    this.message = `Missing choices array for choice field.`;
  }
}

export class NoStoreFound extends Error {
  /**
   * Used in the {@link Models} code when a code path assumes that there is a store available when there isn't.
   * @name Errors.NoStoreFound
   * @member
   */
  constructor() {
    super();
    this.message = `Cannot register models until after Models.setStore(store) has been called.`;
  }
}

export class StoreNotReady extends Error {
  /**
   * Used in the {@link Models} code when a code path assumes that a store is ready for use when it isn't.
   * @name Errors.StoreNotReady
   * @member
   */
  constructor() {
    super();
    this.message = `Store is not ready for use yet.`;
  }
}

export class TypeNotMatchedToChoices extends Error {
  /**
   * Used in the model fields code when a choice field with a <code>default</code> value does not contain that value in the available choices.
   * @name Errors.TypeNotMatchedToChoices
   * @member
   * @param {String} type - Field type for model field.
   */
  constructor(type) {
    super();
    this.message = `Cannot declare ${type} field with non-${type} value unless that value is exists in [options.choices].`;
    this.type = type;
  }
}

export class FieldFailedCustomValidation extends Error {
  /**
   * Used in the model field code to signal that a value did not pass custom validation (even if it passed basic validation).
   * @name Errors.FieldFailedCustomValidation
   * @member
   * @param {String} key - Model field name.
   */
  constructor(key) {
    super();
    this.message = `${key} value failed custom validation.`;
    this.key = key;
  }
}

export class ModelFormDeclarationHasUnknownFields extends Error {
  /**
   * Used in the model registration process when a model with __meta.form code refers to fields that do not exist on the model.
   * @name Errors.ModelFormDeclarationHasUnknownFields
   * @member
   * @param {String} modelName - Model field name.
   * @param {String[]} fields - Array of field names that don't exist
   */
  constructor(missing) {
    super();
    const models = missing.map((v) => v.modelName).join(`, `);
    this.message = `Model${
      models.length > 1 ? `s` : ``
    } ${models} had __meta.form declarations with non-existent fields.`;
    this.missing = missing;
  }
}

export class CouldNotFindModel extends Error {
  /**
   * Used in the {@link ModelRegistry} to signal that the code tried to access an unknown schema.
   * @name Errors.CouldNotFindModel
   * @member
   * @param {String} modelName - Model class name.
   */
  constructor(modelName) {
    super();
    this.message = `Could not retrieve schema for Model ${modelName}, remember to await Models.register(${modelName})`;
    this.modelName = modelName;
  }
}

export class SchemaMismatchForModel extends Error {
  /**
   * Used in the {@link ModelRegistry} to signal that the currently loaded version of a schema does not match the stored version of that same schema.
   * @name Errors.SchemaMismatchForModel
   * @member
   * @param {String} modelName - Model class name.
   */
  constructor(modelName) {
    super();
    this.message = `Schema mismatch for ${modelName} model, please migrate your data first.`;
    this.modelName = modelName;
  }
}

export class ModelCreateWithBadData extends Error {
  /**
   * Used in {@link Model} to signal that .create(data) was passed non-object data
   * @name Errors.ModelCreateWithBadData
   * @member
   * @param {String} modelName - Model class name.
   */
  constructor(modelName) {
    super();
    this.message = `${modelName}.create(data) requires that data is an object.`;
    this.modelName = modelName;
  }
}

export class ModelFromMissingData extends Error {
  /**
   * Used in {@link Model} to signal that .from(data) was called without a data payload.
   * @name Errors.ModelFromMissingData
   * @member
   * @param {String} modelName - Model class name.
   */
  constructor(modelName) {
    super();
    this.message = `${modelName}.from() must be called with a data object.`;
    this.modelName = modelName;
  }
}

export class IncompleteModelSave extends Error {
  /**
   * Used in {@link Model} to signal that the user tried to save a model that is missing required values (something which can only be the case if the model was built using the ALLOW_INCOMPLETE symbol)
   * @name Errors.IncompleteModelSave
   * @member
   * @param {String} modelName - Model class name.
   * @param {String[]} errors - Array of error strings describing all problems found.
   */
  constructor(modelName, errors) {
    super();
    this.message = `Cannot save incomplete ${modelName} model (created with ALLOW_INCOMPLETE).`;
    this.modelName = modelName;
    this.errors = errors;
  }
}

export class DoNotUseModelConstructor extends Error {
  /**
   * Thrown when code tries to construct a model using the <code>new</code> keyword, rather than the <code>create()</code> function.
   * @name Errors.DoNotUseModelConstructor
   * @member
   * @param {String} modelName - Model class name.
   */
  constructor(modelName) {
    super();
    this.message = `Use ${modelName}.create(data?) to build model instances.`;
    this.modelName = modelName;
  }
}

export class BadModelDataSubmission extends Error {
  /**
   * Used in {@link Model} to signal that a data update using a form submission object did not succeed due to validation failure.
   * @name Errors.BadModelDataSubmission
   * @member
   * @param {String} modelName - Model class name.
   * @param {String[]} errors - Array of error strings describing all problems found.
   */
  constructor(modelName, errors) {
    super();
    this.message = `Submitted data did not pass validation for ${modelName} schema.`;
    this.modelName = modelName;
    this.errors = errors;
  }
}

export class UndefinedKey extends Error {
  /**
   * Used in {@link Model} to signal a missing object property that was expect to exist, based on the model schema.
   * @name Errors.UndefinedKey
   * @member
   * @param {String} key - Model field name.
   * @param {String} modelName - Model class name.
   */
  constructor(key, modelName) {
    super();
    this.message = `Property [${key}] is not defined for model ${modelName}.`;
    this.modelName = modelName;
  }
}

export class RequiredFieldsMissing extends Error {
  /**
   * Used in {@link Models} to signal that something tried to build a {@link Model} without specifying values for all <code>required</code> properties in that model.
   * @name Errors.RequiredFieldsMissing
   * @member
   * @param {String} modelName - Model class name.
   * @param {String[]} errors - Array of error strings describing all problems found.
   */
  constructor(modelName, errors) {
    super();
    this.message = `Cannot create ${modelName}: missing required fields (without schema-defined default).`;
    this.modelName = modelName;
    this.errors = errors;
  }
}

export class AssignmentMustBeArray extends Error {
  /**
   * Used in {@link Models} to signal that an assignment to an <code>array</code> property was not an array.
   * @name Errors.AssignmentMustBeArray
   * @member
   * @param {String} key - Model field name.
   */
  constructor(key) {
    super();
    this.message = `Assignment for [${key}] must be an array.`;
    this.key = key;
  }
}

export class InvalidAssignment extends Error {
  /**
   * Used in {@Models} to signal that a property assignment was not permitted according to the model schema.
   * @name Errors.InvalidAssignment
   * @member
   * @param {String} key - Model field name.
   * @param {*} value - Property value for model field.
   * @param {String[]} errors - Array of error strings describing all problems found.
   */
  constructor(key, value, errors) {
    super();
    this.message = `${key} could not be assigned value [${value}].`;
    this.key = key;
    this.value = value;
    this.errors = errors;
  }
}

export class RecordDoesNotExist extends Error {
  /**
   * Used to signal a record cannot be resolved by a backend
   * @name Errors.RecordDoesNotExist
   * @member
   * @param {String} recordIdentifier - The fully qualified name of the record in question.
   * @param {*} details - any number of additional arguments that will be captured as error.details[].
   */
  constructor(recordIdentifier, ...details) {
    super();
    this.message = `Record ${recordIdentifier} does not exist.`;
    this.recordIdentifier = recordIdentifier;
    this.details = details;
  }
}

export class RecordAccessError extends Error {
  /**
   * Used to signal that a backend can find a record, but can't access it.
   * @name Errors.RecordAccessError
   * @member
   * @param {String} recordIdentifier - The fully qualified name of the record in question.
   * @param {*} details - any number of additional arguments that will be captured as error.details[].
   */
  constructor(recordIdentifier, ...details) {
    super();
    this.message = `Could not read file ${recordIdentifier}.`;
    this.recordIdentifier = recordIdentifier;
    this.details = details;
  }
}

export class RecordParseError extends Error {
  /**
   * Used to signal a filepath resolves to a file, but its content could not be parse as model record data.
   * @name Errors.RecordParseError
   * @member
   * @param {filepath} path - Path to a file in the filesystem.
   */
  constructor(path) {
    super();
    this.message = `Could not parse ${path}.`;
    this.path = path;
  }
}

export class MissingRecordNameBinding extends Error {
  /**
   * Used by the schema code to signal that a model could not be saved using an unqualified <code>save()</code> call.
   * @name Errors.MissingRecordNameBinding
   * @member
   * @param {String} modelName - Model class name.
   */
  constructor(modelName) {
    super();
    this.message = `${modelName} model lacks a "__meta.recordName" binding for auto-saving model instances.`;
    this.modelName = modelName;
  }
}

export class MissingImplementation extends Error {
  /**
   * Thrown by anything that intends to be an abstract superclass, to make sure subclasses implement the necessary methods.
   * @name Errors.MissingImplementation
   * @member
   * @param {String} classAndMethodName - String representation of a class.method(signature).
   */
  constructor(classAndMethodName) {
    super();
    this.message = `Missing implementation for ${classAndMethodName}`;
    this.classAndMethodName = classAndMethodName;
  }
}

export class IncorrectSyncType extends Error {
  /**
   * Thrown by anything that intends to be an abstract superclass, to make sure subclasses implement the necessary methods.
   * @name Errors.IncorrectSyncType
   * @member
   * @param {String} classAndMethodName - String representation of a class.method(signature).
   * @param {String} [syncType] - "AsyncFunction" if this is supposed to be an async function, otherwise undefined.
   */
  constructor(classAndMethodName, syncType) {
    super();
    const should = syncType === `AsyncFunction` ? `should` : `should not`;
    this.message = `${classAndMethodName} ${should} be marked async.`;
    this.classAndMethodName = classAndMethodName;
  }
}