Leoric ORM

repository·master·Indexed 42 days ago

https://github.com/cyjake/leoric

A high-performance JavaScript object-relational mapping (ORM) library inspired by Ruby on Rails Active Record. Version 2.16.0 supports multiple SQL dialects including MySQL, PostgreSQL, SQLite, and sqljs. It features a chainable query builder (Spell), model compilation via decorators or Realm definitions, and first-class TypeScript support with @Column and @BelongsTo decorators.

Tokens
65.9K
Snippets
263
Records
317
Agent score
52%

What's inside Leoric

  1. What is Leoric

    master

    Leoric is a thin Object Relational Mapping (ORM) layer for Node.js. It is heavily influenced by Ruby on Rails' Active Record and is designed to represent business data and logic by connecting rich application objects to relational database tables.

    Key features include:

    • Representing models and their data.
    • Representing associations between models.
    • Mapping existing tables without repeating column definitions.
    • Asynchronous CRUD (Create, Read, Update, Delete) operations.
    • Support for modern JavaScript.
  2. Overview of Leoric Architecture

    master

    Leoric is a Node.js ORM inspired by Ruby on Rails Active Record. It uses a hierarchical structure for models and database connectivity:

    • Bone: The base class users inherit from to access typed query methods like find, findOne, sum, restore, and update.
    • AbstractBone: The core implementation of models, handling attributes, associations, lifecycles, and instance methods (e.g., save, remove, reload, toObject).
    • Realm: Manages database connections, model registration via define(), and execution of connect(), sync(), query(), and transaction().
    • Spell: A chainable query builder providing methods like where, order, limit, and with.
    • Drivers: SQL dialect implementations for mysql, postgres, sqlite, and sqljs.

    Dependency Flow:

    • bone $\rightarrow$ abstract_bone $\rightarrow$ drivers
    • spell $\rightarrow$ expr
    • realm $\rightarrow$ drivers
  3. Manage SQLite connection limits and busy errors

    master

    SQLite supports connection pooling, which is enabled by default with a connectionLimit of 10. However, multiple connections can lead to SQLITE_BUSY errors because SQLite lacks a server to manage locks.

    To mitigate this, you can:

    1. Set connectionLimit: 1 to ensure only one connection is used.
    2. Increase the busyTimeout to allow the client to wait longer for locks to clear.
    const realm = new Realm({
      dialect: 'sqlite',
      database: 'database/development.sqlite3',
      models: 'app/models',
      connectionLimit: 1,
    });
  4. Understand Leoric Method Chaining and Spells

    master

    Leoric uses method chaining to build queries. Calling a query method like .find() or .order() returns a Spell instance. A Spell is a builder object that allows you to append further query details (like .where(), .group(), .limit(), etc.) before execution.

    Execution:

    • A Spell can be treated as a Promise (using async/await, .then(), or co).
    • To inspect the generated SQL without executing it, call .toSqlString() on the Spell instance.
    // Building and executing a chain
    const query = Post.where('title LIKE ?', '%Post%')
    const posts = await query.order('updatedAt desc').limit(10)
    
    // Inspecting SQL
    const sql = Post.find().where('id = 1').toSqlString()
  5. Best practices for data manipulation in migrations

    master

    When a migration requires updating existing data (e.g., populating a new column), it is highly recommended to use raw SQL queries via driver.query() rather than using Model APIs.

    Using Model APIs (like User.find()) inside a migration is discouraged because model definitions change over time. If a model's definition evolves, an old migration using that model might break when executed on a new environment. Raw SQL is deterministic and remains stable regardless of model changes.

    module.exports = {
      async up(driver, DataTypes) {
        // 1. Add the column
        await driver.addColumn('users', 'display_name', {
          type: DataTypes.STRING,
        });
    
        // 2. Use raw SQL to populate data (Recommended)
        await driver.query(`
          UPDATE users SET display_name = CONCAT(first_name, ' ', last_name)
        `);
      },
    
      async down(driver, DataTypes) {
        await driver.removeColumn('users', 'display_name');
      },
    };
  6. How VIRTUAL columns work

    master

    The VIRTUAL type allows you to define columns that exist on your model instance but are not stored in the database. These are ideal for computed properties that depend on other attributes.

    class User extends Bone {
      static attributes = {
        firstName: STRING,
        lastName: STRING,
        fullName: {
          type: VIRTUAL,
          get() {
            return `${this.firstName} ${this.lastName}`;
          },
        },
      }
    }
  7. Understand `model.update()` behavior in Sequelize mode

    master
    When using the Sequelize adapter, model.update(values) behaves differently than the default Leoric implementation. While the default implementation only updates the specific values passed in, the Sequelize adapter merges all changes (including the values passed in) and applies the merged set once. Ensure your logic accounts for this merging behavior when updating model instances.
  8. Use Object Conditions for filtering

    master

    Object conditions allow you to filter records using a JavaScript object where keys are field names and values are the criteria.

    • Primitive values: { field: value } performs an equality check. Passing null or undefined results in an IS NULL check.
    • Arrays: { field: [val1, val2] } performs an IN check.
    • Operator Objects: You can use special keys starting with $ to perform comparison operations. If an object contains multiple operators for the same field, they are combined with AND.

    Supported Operators: $eq, $gt, $gte, $lt, $lte, $ne, $in, $nin, $notIn, $like, $notLike, $between, $notBetween.

    // Equality and Null checks
    Post.find({ id: 1 });
    Post.find({ title: null });
    
    // IN clause
    Post.find({ title: ['New Post', 'Untitled'] });
    
    // Operators
    Post.find({ title: { $ne: 'New Post' } });
    Post.find({ title: { $like: '%Post%' } });
    Post.find({ createdAt: { $lt: new Date(2017, 10, 11) } });
    
    // Multiple operators (combined with AND)
    Post.find({ id: { $gt: 0, $lt: 999999 }});
  9. Use logical operators in object conditions

    master

    Leoric supports logical operators in object conditions using a syntax similar to MongoDB's BOSN grammar. You can use two different patterns for logical queries, and they are both nestable:

    1. Top-level logical operators: Using $or or $and at the root of the condition object.
    2. Field-level logical operators: Nesting $or or $and directly under a specific field name.

    Both methods are supported and can be nested within each other.

    // Pattern 1: Top-level $or
    await Post.where({
      $or: [
        { title: 'Nephalem' },
        { title: { $like: 'Angel%' },
      ],
    });
    
    // Pattern 2: Field-level $or
    await Post.where({
      title: {
        $or: [
          'Nephalem',
          { $like: 'Angel%' },
        ],
      },
    });
  10. Define Model Associations

    master

    Associations are declared inside static initialize(). Leoric resolves model names by convention (camelCase of the target model name). Use the className option if the target name cannot be inferred.

    Supported Association Types

    • this.hasMany('associationName')
    • this.belongsTo('associationName', { className: 'TargetModel' })
    • this.hasMany('associationName', { through: 'otherModel' }) (for many-to-many via a through table)

    Eager Loading

    Use the .with('associationName') method on a query to load associated models.

    // Eager loading example
    const shops = await Shop.find().with('items');
    console.log(shops[0].items); // => [ Item, ... ]
    class Shop extends Bone {
      static initialize() {
        this.hasMany('items');
      }
    }
    
    class Item extends Bone {
      static initialize() {
        this.belongsTo('shop');
      }
    }
    
    // eager loading
    const shops = await Shop.find().with('items');
    console.log(shops[0].items); // => [ Item, ... ]
  11. How Soft Delete interacts with Associations and Timestamps

    master

    Associations

    When soft delete is enabled on a model, its associations respect the deletedAt scope. When using .include() or .with(), any associated records that have been soft-deleted will be automatically filtered out of the result set.

    Timestamps

    • Soft-deletion: Sets deletedAt to the current timestamp. It does not automatically update the updatedAt column.
    • Restoration: Sets deletedAt to null.
  12. Leoric Naming Conventions

    master

    Leoric uses a 'Convention over Configuration' approach to map models to database tables and attributes. If you follow these rules, you write less configuration code.

    Model and Table Mapping

    • Model names: Use CamelCase (e.g., Shop, TagMap).
    • Table names: Use the pluralized model name in snake_case (e.g., shops, tag_maps). Leoric uses the pluralize library for this transformation.

    Attribute Mapping

    • Model attributes: Use camelCase (e.g., createdAt).
    • Database columns: Typically snake_case (e.g., created_at). Leoric automatically transforms column names to camelCase for model attributes.

    Relationship Keys

    • Foreign keys: Should follow the pattern modelNameId (e.g., shopId), which maps to the database column model_name_id (e.g., shop_id).
    • Primary keys: Should be an unsigned integer named id.