Joist ORM

website·Indexed 32 days ago

https://joist-orm.io

An ORM focused on domain modeling and entity-based productivity, inspired by ActiveRecord. Joist features code generation, an EntityManager for loading and saving entities, and built-in support for avoiding N+1 queries via a dataloader-based architecture. It provides advanced capabilities including Class Table Inheritance, optimistic locking, reactive fields, and a flexible query API for both ergonomic find queries and low-level SQL operations.

Tokens
55.5K
Snippets
272
Records
373
Agent score
66%

What's inside Joist

  1. Overview of Joist TypeScript ORM

    Joist is a TypeScript ORM optimized for 'Majestic Monoliths' and Postgres. It focuses on high performance through bulk INSERT and UPDATE operations and batched SELECT reads. Key architectural goals include preventing N+1 query problems via batched and pre-loading, and providing type-safe domain models that can track relation loaded-ness within the TypeScript type system.
  2. Introduction to Joist ORM

    Joist is a TypeScript ORM designed to facilitate the creation of robust domain models. It aims to provide high productivity (similar to ActiveRecord) while solving common backend challenges such as N+1 query problems and state synchronization. Key capabilities include:

    • Code Generation: Reduces boilerplate to accelerate development.
    • N+1 Prevention: Integrated first-class support for dataloader to prevent inefficient database querying.
    • Load-Safe Relations: Type-safe tracking to distinguish between loaded and unloaded relations.
    • Backend Reactivity: Support for reactive fields on the server side.
    • Domain Modeling: Tools for building robust domain-driven designs.
    • Testing Support: Built-in factories and utilities to simplify testing.
  3. Overview of Joist Reactive Fields

    Reactive Fields are values calculated from other data within a domain model that are stored in the database as regular primitive columns. Unlike Derived Fields, they are not calculated on-the-fly during access, making them high-performance 'instantly-updated materialized views'. Joist automatically keeps these fields up-to-date during em.flush by watching 'reactive hints' and recalculating values atomically within the same transaction. Note that reactivity requires all writes to go through the domain model rather than raw SQL updates.
  4. Overview of Joist ORM Reactions

    Reactions are a feature used to run custom business logic whenever specific fields or relations change during a flush or recalculation. They sit between Lifecycle Hooks and Reactive Fields. Unlike Reactive Fields, reactions can make arbitrary changes to any entity. Unlike Lifecycle Hooks, they only run when their specific 'hint' (the fields they watch) changes, and they can run multiple times per flush as the reactivity graph settles.
  5. Understand Joist Load-Safe Relation access

    Joist employs a hybrid approach to relation loading to balance type safety and developer experience:

    1. Async by Default: Any data whose 'loaded-ness' is uncertain must be accessed via await.
    2. Synchronous Access via Hints: When 'populate hint mapped types' are used, the TypeScript compiler can verify that specific data is loaded, allowing it to be accessed synchronously without await.
  6. Determine if Joist is the right fit for your project

    Joist is designed for any Postgres database schema and TypeScript team, drawing inspiration from ActiveRecord. It is particularly suited for moderately-to-very complicated business domains, such as enterprise back-office systems. It is ideal for medium-to-large database schemas where complex business invariants must be managed via reactive validation rules, reactive fields, and lifecycle hooks, rather than simple single-column updates.
  7. Understand how Joist implements Optimistic Locking

    Joist uses optimistic locking to prevent conflicting or dropped UPDATE statements. Instead of locking rows at the database level during a read (pessimistic locking), Joist assumes no conflict will occur. When writing data, Joist verifies that the row has not changed since it was last read by including the updated_at timestamp in the WHERE clause of the UPDATE statement.

    If the UPDATE modifies 1 row, the write is successful. If it modifies 0 rows, it indicates another thread changed the data, and Joist throws an Oplock failure error.

    -- Conceptual SQL generated by Joist
    UPDATE authors
      SET
        first_name = 'bob'
        updated_at = '...10:01am...'
      WHERE id = 1
      AND updated_at = '...10:00am...'
  8. Disable factory defaults

    You can opt-out of factory-provided defaults using the useFactoryDefaults option:

    • useFactoryDefaults: false: Ignores the specific defaults defined in the factory file (e.g., newAuthor.ts), but still enforces Joist's fundamental "required fields must be set" defaults.
    • useFactoryDefaults: "none": Disables all defaults, including required field defaults, leaving them undefined.
    // Ignore factory-defined defaults
    const a = newAuthor(em, { useFactoryDefaults: false });
    
    // Ignore ALL defaults (including required fields)
    const b = newBook(em, { useFactoryDefaults: "none" });
  9. Understand Joist's entity and class philosophy

    Joist uses TypeScript classes (typically following the class Author extends AuthorCodegen pattern) to inject code-generated getters and setters into entities. Unlike strict OOP, Joist entities are designed to expose state to clients and business logic rather than hide it. Classes are used as a natural place to implement helper methods that calculate cross-entity derived fields, effectively overlaying logic on top of the raw POJOs returned from the database.
  10. Understand the performance impact of eager class fields in Joist

    In Joist, relations are declared as class fields (e.g., books = hasMany("books")). Because JavaScript class fields are eagerly initialized, every time an entity is instantiated via new, all declared relations are created and allocated in memory, even if they are never accessed. In complex domain models with many relations (e.g., 30+ relations across 1,000 rows), this eager allocation creates significant performance overhead.
    class Author {
      // This is eagerly initialized on every 'new Author()'
      books = hasMany("books");
    }
  11. Work with relations modeled as objects

    In Joist, relations are not raw arrays but wrapper objects (e.g., Collection<Author, Book[]>). This design ensures type safety by explicitly tracking whether relations have been loaded from the database. To access the data, you must call .load() and .get on the collection object, allowing for incremental loading of data as needed by the business logic.
  12. Implement Single Table Inheritance (STI) in Joist

    Single Table Inheritance allows multiple logical entities (e.g., Dog, Cat) to be mapped to a single physical SQL table (e.g., animals). Joist uses a discriminator column (which must be an enum) to determine the specific subtype of a row. Subtype-specific columns in the database must be nullable because they are omitted during the insertion of other subtypes.