Population
Population in Nobox allows you to fetch related data from other record spaces in a single query, similar to SQL JOINs but with the flexibility of NoSQL. This feature enables you to create rich, relational-like data structures without the complexity of traditional database joins.
Overview
Population works by defining relationships between record spaces using field mappings. When you perform a find()
or findOne()
operation, you can specify population options to automatically fetch related data and include it in your results.
Population Structure
The population is defined in the options
parameter using the populate
property:
export type Options<T extends CObject> = { // ... other options populate?: { fields: { from: string; // Source space name foreignKey: string; // Field in source space to match localKey: string; // Field in current record to match against newField: string; // New field name to add populated data }; space: string; // Target space name }[]; };
Parameters
populate
Array
An array of population configurations, each containing:
fields.from
(string): The name of the source record space to fetch data from. This is thespace
property from your schema definition (e.g., "user", "category", "profile")fields.foreignKey
(string): The field name in the source space to match againstfields.localKey
(string): The field name in the current record to match with the foreign keyfields.newField
(string): The name of the new field that will contain the populated datafields.multi
(boolean, optional): Whentrue
, returns an array of related records. Whenfalse
or omitted, returns a single record. Defaults tofalse
.space
(string): The target space name (usually same asfrom
). This should match thespace
property from your schema definition
Where Do the Values Come From?
The from
and space
values come from the space
property in your schema definitions. Here's how to find them:
Schema Definitions
// User Schema export const UserStructure: Space<User> = { space: "user", // ← This is your "from" value description: "User records", structure: { /* ... */ } }; // Profile Schema export const ProfileStructure: Space<Profile> = { space: "profile", // ← This is your "from" value description: "User profiles", structure: { /* ... */ } }; // Category Schema export const CategoryStructure: Space<Category> = { space: "category", // ← This is your "from" value description: "Post categories", structure: { /* ... */ } };
Using in Population
// When populating, use the "space" values from your schemas const postsWithDetails = await PostModel.find( {}, { populate: [ { fields: { from: "user", // ← From UserStructure.space localKey: "authorId", foreignKey: "id", newField: "author" }, space: "user" // ← Same as "from" }, { fields: { from: "category", // ← From CategoryStructure.space localKey: "categoryId", foreignKey: "id", newField: "category" }, space: "category" // ← Same as "from" } ] } );
Relationship Types
One-to-One Relationship
When each record in the current space has at most one related record in the source space.
// Example: User → Profile (one user has one profile) { fields: { from: "profile", // ← From ProfileStructure.space localKey: "id", // User.id foreignKey: "userId", // Profile.userId newField: "profile" // Result: user.profile }, space: "profile" // ← Same as "from" }
Many-to-One Relationship
When multiple records in the current space can relate to the same record in the source space.
// Example: Post → User (many posts can belong to one user) { fields: { from: "user", localKey: "authorId", // Post.authorId foreignKey: "id", // User.id newField: "author" // Result: post.author }, space: "user" }
Examples
Example 1: Basic Population
// Define schemas interface Post { id: string; title: string; content: string; authorId: string; } interface User { id: string; name: string; email: string; } // Find posts with author information const postsWithAuthors = await PostModel.find( { authorId: "user123" }, { populate: [ { fields: { from: "user", localKey: "authorId", foreignKey: "id", newField: "author" }, space: "user" } ] } ); // Result: // [ // { // id: "post1", // title: "Getting Started with Nobox", // content: "Nobox is a powerful BaaS solution...", // authorId: "user123", // author: { // id: "user123", // name: "John Doe", // email: "john@example.com" // } // } // ]
Example 2: Multiple Population Fields
// Find posts with both author and category information const postsWithDetails = await PostModel.find( {}, { populate: [ { fields: { from: "user", localKey: "authorId", foreignKey: "id", newField: "author" }, space: "user" }, { fields: { from: "category", localKey: "categoryId", foreignKey: "id", newField: "category" }, space: "category" } ] } );
Example 3: One-to-Many Population
// Find users with all their posts (one-to-many) const usersWithPosts = await UserModel.find( {}, { populate: [ { fields: { from: "post", localKey: "id", foreignKey: "authorId", newField: "posts", multi: true // ← Returns array of posts }, space: "post" } ] } ); // Result: // [ // { // id: "user123", // name: "John Doe", // email: "john@example.com", // posts: [ // { id: "post1", title: "First Post", authorId: "user123" }, // { id: "post2", title: "Second Post", authorId: "user123" } // ] // } // ]
Example 4: Real-World Use Case
Based on a practical blog system implementation:
// Define record structures interface Post { id: string; title: string; content: string; authorId: string; categoryId: string; publishedAt: string; } interface User { id: string; name: string; email: string; avatar: string; } interface Category { id: string; name: string; description: string; } // Fetch posts with populated data const getPostsWithDetails = async (userId?: string) => { const posts = await PostModel.find( userId ? { authorId: userId } : {}, { populate: [ { fields: { from: "user", localKey: "authorId", foreignKey: "id", newField: "author" }, space: "user" }, { fields: { from: "category", localKey: "categoryId", foreignKey: "id", newField: "category" }, space: "category" } ] } ); return posts.map((post: any) => ({ ...post, authorName: post.author?.name, authorEmail: post.author?.email, categoryName: post.category?.name })); };
Best Practices
1. Field Naming
- Use descriptive names for
newField
to clearly indicate the relationship - Consider using camelCase for consistency with JavaScript conventions
2. Performance Considerations
- Population adds complexity to queries, so use it judiciously
- Consider the number of related records that will be fetched
- For large datasets, consider pagination
3. Type Safety
- Define interfaces for populated data to maintain type safety
- Use TypeScript interfaces to describe the expected structure
interface PostWithAuthor extends Post { author?: User; category?: Category; }
4. Error Handling
- Handle cases where related data might not exist
- Use optional chaining when accessing populated fields
const authorEmail = post.author?.email || 'No email available';
Method Support
Population is supported by the following methods:
- ✅
find()
: Full population support - ✅
findOne()
: Full population support - ❌
search()
: Population not supported - ❌
insert()
: Population not supported - ❌
insertOne()
: Population not supported - ❌
updateOne()
: Population not supported - ❌
updateOneById()
: Population not supported - ❌
deleteOneById()
: Population not supported
Limitations
- Single Query: Population happens in a single query, so all related data is fetched at once
- No Nested Population: Currently, you cannot populate data from already populated fields
- No Aggregation: Population is for fetching related records, not for aggregating data
- Limited Method Support: Only
find()
andfindOne()
methods support population
Quick Reference
Finding Space Names
// Look at your schema definitions: export const UserStructure: Space<User> = { space: "user", // ← Use this as "from" and "space" // ... }; export const CategoryStructure: Space<Category> = { space: "category", // ← Use this as "from" and "space" // ... };
Population Template
{ fields: { from: "SPACE_NAME", // ← From your schema.space localKey: "FIELD_IN_CURRENT_RECORD", foreignKey: "FIELD_IN_SOURCE_RECORD", newField: "NEW_FIELD_NAME", multi: false // ← true for arrays, false for single record }, space: "SPACE_NAME" // ← Same as "from" }