Member Junction
    Preparing search index...

    Class RelatedRecordCollection<T>

    A typed collection of child records that travels, validates and persists with its parent.

    Obtain one via BaseEntity.DeclareRelatedRecords() in a subclass constructor or field initialiser — do not construct it directly, or it will not be registered as a companion and will be silently ignored by load, validation and save.

    @RegisterClass(BaseEntity, 'MJ_BizApps_Accounting: Journal Entries')
    export class JournalEntryEntity extends mjBizAppsAccountingJournalEntryEntity {
    public readonly Lines = this.DeclareRelatedRecords<JournalEntryLineEntity>({
    Name: 'Lines',
    RelatedEntity: 'MJ_BizApps_Accounting: Journal Entry Lines',
    RelatedEntityJoinField: 'JournalEntryID',
    OrderBy: 'LineNumber ASC',
    Load: 'explicit',
    OnRemove: 'delete',
    Sequence: { Field: 'LineNumber', From: 1 },
    });

    public override Validate(): ValidationResult {
    const result = super.Validate(); // fans out to companions
    assertBalanced(this.Lines.Items, result); // runs on BOTH tiers
    return result;
    }
    }

    Type Parameters

    Hierarchy (View Summary)

    Index

    Constructors

    Properties

    Owner: BaseEntity

    The entity this companion is attached to. Set by BaseEntity.RegisterCompanion.

    Accessors

    • get Count(): number

      Number of retained related records.

      Deliberately delegates to Items rather than reading the backing array: for a 'lazy' collection Items is what triggers population, so reading the raw array here would report 0 for a collection that has simply not been touched yet — and Count === 0 while Items.length === 2 is the kind of inconsistency nobody debugs quickly. Same reason it picks up a live cache view's refresh.

      Returns number

    • get IsAvailable(): boolean

      Whether reading Items right now will succeed — the guard for display-tier code.

      A lazy collection's Items getter throws when its donor engine is not available, deliberately: a silently empty array is how the bug this feature replaced went unnoticed for years. That is the right default for business logic, but a template or widget rendering during bootstrap (before anything has awaited the engine's Config()) wants "not yet", not an aborted render — and the null-check templates reach for (@if (entity.Params && …)) cannot help, because the collection property itself is never null; it is the read that throws.

      @if (action.Params.IsAvailable) {
      @for (p of action.Params.Items; track p.ID) { … }
      }

      This is a predicate, not a second way to read. There is exactly one accessor — Items — so there is no null-versus-[] ambiguity for a caller to get wrong, and no quiet path that can drift into business logic and re-create the silent-empty bug. true here means the very next Items read is safe and already populated, because deciding the answer requires consulting the donor, and consulting it is what fills the collection.

      Never triggers a database load, and never throws.

      Returns boolean

    • get IsReadOnly(): boolean

      Whether this collection refuses mutation.

      Defaults to false, except for a cache-sourced collection, which defaults to true because its records are the engine's own shared instances. An explicit ReadOnly: false still wins — and switches the cache path to copying, so the engine's objects stay untouched.

      Returns boolean

    • get Items(): readonly T[]

      The retained children, in order.

      Read-only by design — mutate through Add, Create and Remove so that removals are tracked, sequence numbers stay correct, and the parent's Dirty flag reflects reality. Handing out a mutable array would make all three impossible to guarantee.

      Returns readonly T[]

    • get Name(): string

      Stable identifier for this companion, unique within its owning entity.

      This is the wire key: it appears in serialized payloads and is how the receiving tier finds the companion to deserialize into. Treat it as a published contract — renaming it breaks in-flight payloads and any persisted snapshot that captured them.

      Returns string

    Methods

    • Iterates the retained records, so the collection works directly with for…of, spread and array destructuring:

      for (const param of action.Params) { … }
      const all = [...action.Params];
      const [first, ...rest] = action.Params;

      This is the standard ES2015 iterable protocol — the same one Map, Set and NodeList implement — deliberately chosen over extending Array. Subclassing Array would inherit push, splice, sort and index assignment, every one of which bypasses the removal tracking, foreign-key stamping and sequence renumbering this class exists to guarantee; and Symbol.species would hand map/filter this constructor, which takes an owner and options rather than a length. Iterability adds the ergonomics without any of that.

      Use Items when you want the array itself — map, filter, find and indexing. It is readonly, which is what keeps a caller from mutating around the collection's back.

      Returns Iterator<T>

      An iterator over the retained records, in collection order.

    • Contributes this companion's work to the owner's save plan.

      Called after the owner's own node has been added, so implementations may assume the parent node exists and order their nodes relative to it. Add nothing when there is no work — an empty contribution keeps the save on the fast single-row path.

      Parameters

      • plan: EntitySavePlan

        The plan being assembled for this unit of work.

      • Optionaloptions: EntitySaveOptions

        The caller's save options. Implementations that decide what counts as work (skipping clean children, most importantly) must honor flags such as IgnoreDirtyState that demand a full write-out.

      Returns void

    • Creates a new, empty child entity, appends it, and returns it.

      Uses the owner's provider so the child resolves to the right registered subclass on whichever tier this runs — the server subclass on the server, the shared subclass in the browser.

      Returns Promise<T>

      The newly created child.

    • Populates the collection from the database.

      A no-op when the parent is unsaved (there is nothing to be a child of) or when LoadMode is 'never'.

      Parameters

      • force: boolean = false

        Reload even if already loaded, discarding any unsaved children and removals.

      Returns Promise<void>

      A failed load throws rather than yielding an empty collection. Silently returning no children makes a populated parent look empty, and anything derived from that — a reversal, a total, a validation decision — is then wrong in a way nothing downstream can detect. Only saves use the boolean-return convention.

      Loading over UNSAVED WORK also throws, for the same reason. Add() and Create() do not mark a collection loaded, so a collection that has only ever been appended to still has loaded === false — and the early return above therefore does not protect it. A Load() from anywhere (a lazy read, a refresh, a sibling component) would replace items wholesale and take the caller's unsaved children with it, along with any queued deletions. Nothing reports that; the screen simply shows fewer rows than the user typed.

      Pass force to discard deliberately — that is what a refresh means, and saying so is cheap.

      When the collection has unsaved changes and force is not set.

    • Populates this companion from the database, when it is configured to load eagerly.

      Called by BaseEntity.Load() after the record's own fields are populated. Never called from LoadFromData() — that is the row-materialization path for RunView(ResultType:'entity_object'), so loading children there turns one view into an N+1 storm. Set-oriented eager loading is handled by RunView's batched child loading instead.

      Parameters

      • Optional_visited: Set<string>

        EntityName:PK tokens already on this load walk. Embedded records use it to fail a self-parented / cyclic inherit instead of recursing until the stack dies.

      Returns Promise<void>

    • Replaces an existing item in the collection with another item (e.g. replacing a generic base entity with its hydrated polymorphic IS-A leaf entity instance).

      Parameters

      • oldItem: T

        The item currently in the collection to replace.

      • newItem: T

        The replacement item.

      Returns boolean

      True if the item was found and replaced, false otherwise.

    • Replaces the collection's contents with rows already fetched elsewhere.

      Used by RunView's batched child loading, which issues one WHERE fk IN (...) for an entire result set and distributes the rows — turning what would be N+1 queries into 1 + K.

      Parameters

      • items: T[]

        The children belonging to this parent.

      Returns void

    • Attempts to populate this collection from a BaseEngine cache without touching the database.

      Used by BaseEntity.LoadRelatedRecords() to resolve the free collections before batching whatever is left into a database round trip.

      Returns Promise<boolean>

      True when the collection was populated from a cache; false when the caller must load it from the database.

    • Synchronous, in-memory validation contributed by this companion.

      Runs as part of the owner's Validate(), before any write, over the companion's complete state — including pending removals. That ordering is what lets cross-child invariants such as "debits must equal credits" be enforced correctly rather than after half the graph has landed.

      Push errors onto result.Errors and set result.Success = false to fail the save.

      Parameters

      Returns void

    • Asynchronous validation contributed by this companion — anything that needs a round trip.

      Parameters

      Returns Promise<void>

      Unlike an entity's own ValidateAsync(), this is not governed by BaseEntity.DefaultSkipAsyncValidation. That flag exists so an entity can opt out of its own expensive async rules; applying it to companions silently skipped cross-child invariants, which is how OrderEntityServer.ValidateAsync came to be dead code on every save. Companion validation runs whenever the companion is dirty.