ChatiumFor developersPlaygroundPricing
Sign in

Working with Relationships (Links) Between Records in Heap Tables

When modeling data, there often arises the task of declaring a reference field in a table that contains a pointer to another record in a different or the same (for example, when modeling a tree hierarchy) heap table. This can be achieved by simply storing the record identifier as a string field. However, heap tables support two special types of fields:

Using these types provides the following conveniences:

  • They accept heap objects as input, not just identifiers.
  • Validation of the identifier and type (for RefLink) of the table record.
  • Integrity control (which can be disabled): it is impossible to delete a record that is "referenced" by another record.
  • At runtime, the values of such fields are represented by instances of special classes that support the method .get(ctx), which allows for convenient reading of the referenced record.
  • Simplifies the creation of automatic editing interfaces for table records (admin panels).
  • Facilitates code readability due to precise semantic designation of the field's purpose.

Description and Ways to Declare RefLink

RefLink is a complete analog of foreign keys in relational databases and allows modeling relationships between entities. Unlike relational databases, a RefLink type field can exist not only at the top level of a table but also at any depth within nested objects and arrays. When declaring a field using the Heap.RefLink function, it is necessary to specify the heap table whose records will be referenced by the field values. This can be done in one of two ways:

  • Directly pass the repository of the target table. This is the recommended approach. It is more efficient from an internal implementation perspective and has fewer chances of error. Additionally, this is the only available method if the table is declared in the code of another account. However, this method does not allow creating cyclic references.

    import { Heap } from '@app/heap'
    import { Products } from '@external/heap'
    const Deals = Heap.Table('deals', {
      product: Heap.RefLink(Products),
    })
    
  • Specify a string with the name of the target table. This method is recommended only in cases of cyclic references, for example, when it is necessary to model a hierarchy using a parent field that "points" to its own table.

    import { Heap } from '@app/heap'
    const Tree = Heap.Table('tree', {
      parent: Heap.Nullable(Heap.RefLink('tree')),
    })
    

Due to their dynamic nature, reference fields do not support default values. When adding to existing non-empty heap tables, they should be wrapped in Heap.Optional or Heap.Nullable.

When declaring a RefLink, it is important to use the directly imported Heap object; that is, you cannot use a derived variable with a different name or one passed as an argument to the function.
This is necessary because, during the source code compilation stage, a special transformer finds such fields and "appends" service information for correct operation.

Reference Integrity Control

The use of RefLink and GenericLink type fields automatically enables reference integrity control. This means that the system will not allow the deletion of a record that is referenced by any RefLink or GenericLink field by default. If for some reason this behavior is problematic and the developer "knows what they are doing," it can be turned off for each field individually by passing the option onDelete: 'none' as the last argument when declaring the field (for more details on the option, see Heap.RefLink and Heap.GenericLink). It should be understood that when disabling integrity control, the field values may contain references to non-existent records, and using the method RefLink.get or getById will throw runtime exceptions, which are likely to be unexpected.

<!-- TODO RefLink.get -->

Support for onDelete: 'cascade' and onDelete: 'set-null', which are typically present in relational databases, is not implemented for heap tables.

GenericLink and Its Differences from RefLink

GenericLink is very similar to RefLink, but applicable in a much narrower set of scenarios. Examples include:

  • A field in a support incident table that can contain a link to an object in the company's information system for which this incident was created.
  • A "set of links" field in a document management system table that contains a list of links to different objects in the information system related to this document.

Features of Recording GenericLink Values

Unlike RefLink, where information about the type (table) of the referenced record is contained in the table schema, in GenericLink, this information is stored directly in the field value alongside the record identifier. Therefore, in create/update operations, values of type GenericLink cannot be passed as simple identifiers; only the actual object being referenced or an instance of the GenericLink class obtained from a previous query is supported.

const Products = Heap.Table('products', {
  refLink: Heap.RefLink(SomeTable),
  genLink: Heap.GenericLink(),
})
const someRecord = await SomeTable.findOneBy(ctx)
await Products.create(ctx, {
  // OK
  refLink: someRecord.id,
  // error!
  genLink: someRecord.id,
  // correct:
  genLink: someRecord,
})

Features of Working with GenericLink Values

Since the code generally "does not know" what type of record is being referenced when querying a GenericLink field, working with GenericLink values is always somewhat more complex than with RefLink values and involves branching based on the specific record type.

Here, the properties and methods of the helper class GenericLink, whose instances represent the values, come to the rescue. Branching can be organized in the following ways:

  • Using the properties GenericLink.type and HeapTableRepo.type. By comparing the value of the type property of the GenericLink field with the type property of one of the possible target tables, you can determine the table to which the identifier stored in the GenericLink field belongs and use the method getById to retrieve the target record:
import { Table1, Table2 } from './tables'
const Products = Heap.Table('products', {
  genLink: Heap.GenericLink(),
})
const product1 = await Products.findOneBy(ctx)
if (product1.genLink.type === Table1.type) {
  const linkRecord = await Table1.getById(ctx, product1.genLink.id)
  // ... Table1 related logic
} else if (product1.genLink.type === Table2.type){
  const linkRecord = await Table2.getById(ctx, product1.genLink.id)
  // ... Table2 related logic
}
  • Using the methods GenericLink.get() and HeapTableRepo.isMyRecord(). The method GenericLink.get() is smart enough to find the appropriate table and perform the selection of that table's record. However, to "understand" what type of record is ultimately returned, it is still necessary to manually iterate through the options using the method isMyRecord, which is also a type predicate and "casts" the record to the correct type at the TypeScript code level:
import { Table1, Table2 } from './tables'
const Products = Heap.Table('products', {
  genLink: Heap.GenericLink(),
})
const product1 = await Products.findOneBy(ctx)
const linkRecord = await product1.genLink.get(ctx)
if (Table1.isMyRecord(linkRecord)) {
  // ... Table1 related logic
} else if (Table2.isMyRecord(linkRecord)){
  // ... Table2 related logic
}

Modeling Many-to-Many Relationships

Unlike relational databases, it is not necessary to create a separate heap table to model many-to-many relationships. In most cases, it will be much simpler to declare an "array of links":

import { Heap } from '@app/heap'
const Tags = Heap.Table('tags', {
  name: Heap.String(),
})
const Products = Heap.Table('products', {
  tags: Heap.Array(Heap.RefLink(Tags)),
})
const product1 = await Products.findOneBy(ctx)
const product1Tags = await Promise.all(
  product1.tags.map(link => link.get(ctx))
)

Filtering

Reference fields support filtering:

  • By the identifier of the target record:

    import { Table1, Table2 } from './tables'
    const Products = Heap.Table('products', {
      refLink: Heap.RefLink(Table1),
      genLink: Heap.GenericLink(),
    })
    const table1Record = await Table1.findOneBy(ctx)
    const table2Record = await Table2.findOneBy(ctx)
    
    await Products.findBy(ctx, { 
      refLink: table1Record.id,
      genLink: table2Record.id,
    })
    
  • By a list of identifiers (like IN in SQL):

    await Products.findBy(ctx, { 
      refLink: [table1Record.id],
      genLink: [table1Record.id, table2Record.id],
    })
    
  • For GenericLink, filtering by type (target table) and by a list of types is also supported:

    await Products.findBy(ctx, { 
      genLink: { type: Table1.type },
    })
    await Products.findBy(ctx, { 
      genLink: { type: [Table1.type, Table2.type] },
    })
    

When filtering by identifier, the following filter values are accepted:

  • Directly a string identifier (as shown in the examples above).

  • An instance of RefLink or GenericLink.

    const product = await Table1.findOneBy(ctx)
    
    await Products.findBy(ctx, { 
      refLink: product.refLink,
      genLink: product.genLink,
    })
    
  • The entire target record.

    await Products.findBy(ctx, { 
      refLink: table1Record,
      genLink: table2Record,
    })
    
  • Any combination of the above in a list.

    await Products.findBy(ctx, { 
      refLink: [table1Record.id, product.refLink, table1Record],
      genLink: [table1Record.id, product.refLink, table2Record],
    })
    

Comparison operators for reference fields are not supported.
Operators $not, $and, $or, and $noop work as they do elsewhere.