Guide

Relations

Declare direct relationships and load them explicitly in batches.

Relations belong to metadata. They do not create PostgreSQL foreign keys and are only loaded when requested through populate.

Many to one

{
  name: 'owner',
  kind: 'manyToOne',
  target: 'users',
  foreignKey: 'ownerId',
  references: 'id'
}

The source entity stores ownerId. The resolver collects those values and loads matching target rows.

One to one

{
  name: 'profile',
  kind: 'oneToOne',
  target: 'profiles',
  foreignKey: 'profileId',
  references: 'id'
}

Resolution is the same as many-to-one, but the metadata expresses a different domain cardinality.

One to many

{
  name: 'projects',
  kind: 'oneToMany',
  target: 'projects',
  foreignKey: 'ownerId',
  references: 'id'
}

The resolver collects source id values, loads targets whose ownerId is in that set, then groups rows by foreign key.

Many to many

{
  name: 'teams',
  kind: 'manyToMany',
  target: 'teams',
  through: {
    table: 'teamMembers',
    sourceForeignKey: 'userId',
    targetForeignKey: 'teamId'
  },
  sourceKey: 'id',
  targetKey: 'id'
}

The junction table must be registered and built like every other table.

Populate

const users = await engine.findMany('users', {
  populate: ['projects', 'teams']
})

For findOne(), the same populate option applies after loading the base entity.

  • to-one relations resolve to an entity or null;
  • to-many relations resolve to an array;
  • missing relation names throw;
  • duplicate keys are deduplicated before related queries.

Query behavior

Each populated relation results in batched adapter queries, not one query per source entity. Many-to-many requires one junction query and one target query.

Population is explicit so callers can see when a query will perform additional database work.

Every populated target passes through its own table serialization rules. A hidden password on users, for example, remains hidden when a user is loaded through projects.owner.

Current scope

The resolver does not currently support:

  • nested paths such as projects.owner;
  • filters or sorting per populated relation;
  • relation-specific pagination;
  • field projections;
  • automatic recursive loading.

PostgreSQL introspection also returns relations: []; declared relation metadata remains the source of truth.