Relations belong to metadata. They do not create PostgreSQL foreign keys and are only loaded when requested through populate.
{
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.
{
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.
{
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.
{
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.
const users = await engine.findMany('users', {
populate: ['projects', 'teams']
})
For findOne(), the same populate option applies after loading the base entity.
null;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.
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.
The resolver does not currently support:
projects.owner;PostgreSQL introspection also returns relations: []; declared relation metadata remains the source of truth.