npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@arbel/firebase-orm

v1.9.79

Published

Firestore Orm

Readme

Arbel Firebase ORM

npm version

Arbel Firebase Orm is an ORM that can run in NodeJS, Browser, Cordova, PhoneGap, Ionic, React Native, NativeScript, Expo, and Electron platforms and can be used with TypeScript and JavaScript (ES5, ES6, ES7, ES8).

Firebase ORM supports only Active Record pattern for now.

Some Arbel Firebase Orm features:

  • 🚀 ActiveRecord Pattern - Intuitive object-oriented database interaction
  • 🔗 Comprehensive Relationships - One-to-one, one-to-many, many-to-many with lazy loading
  • 🏗️ Hierarchical Data Structure - Complex reference paths for nested collections (websites/:website_id/members)
  • 🛡️ Type Safety - Full TypeScript support with compile-time validation
  • Real-time Updates - Live data synchronization with automatic model hydration
  • 🔍 Advanced Querying - Chainable queries with text search and indexing capabilities
  • 🔧 Cross-Platform - Same API works in browser, Node.js, and Firebase Functions
  • 📊 Performance Optimized - Lazy loading, caching, and efficient relationship handling
  • 🛠️ Lifecycle Hooks - beforeSave, afterSave, beforeDestroy, afterDestroy
  • 📝 Automatic Timestamps - created_at and updated_at fields managed automatically

And more...

📚 Documentation

For comprehensive guides and examples, visit our Documentation:

Getting Started

Framework Integration

  • Angular - Services, components, and dependency injection
  • Next.js - SSR, SSG, client-side, and API routes
  • Nuxt.js - Vue.js with SSR/SSG support
  • React - Hooks, context, and state management
  • Vue.js - Composables and reactive data
  • Node.js - Express APIs and backend services
  • Firebase Functions - Server-side functions, triggers, and APIs

Core Features

Advanced Topics

🚀 Quick Example

With Firebase ORM your models look like this:

import { Field, BaseModel, Model } from "@arbel/firebase-orm";

// Simple model
@Model({
  reference_path: "users",
  path_id: "user_id"
})
export class User extends BaseModel {
  @Field({ is_required: true })
  public name!: string;

  @Field({ is_required: true })
  public email!: string;

  @Field({ field_name: "created_at" })
  public createdAt?: string;
}

// Complex hierarchical model
@Model({
  reference_path: "websites/:website_id/members",  // Nested structure
  path_id: "member_id"
})
export class Member extends BaseModel {
  @Field({ is_required: true })
  public name!: string;

  @Field({ field_name: "photo_url" })
  public photoUrl!: string;

  @Field({ is_required: false })
  public role?: string;
}

And your domain logic looks like this:

// Create a new user
const user = new User();
user.name = "John Doe";
user.email = "[email protected]";
user.createdAt = new Date().toISOString();
await user.save();

// Work with hierarchical data
const website = await Website.findOne('domain', '==', 'www.google.com');
const members = await website.getModel(Member).getAll(); // Use getModel() for nested collections
console.log(`${website.domain} has ${members.length} members`);

// Query users
const activeUsers = await User.query()
  .where('isActive', '==', true)
  .orderBy('createdAt', 'desc')
  .limit(10)
  .get();

// Real-time updates
const unsubscribe = User.onList((user) => {
  console.log('User updated:', user.name);
});

// Relationships
const posts = await user.loadHasMany('posts');
console.log(`${user.name} has ${posts.length} posts`);

// Text search
const searchResults = await User.query()
  .like('name', '%john%')
  .get();

⚡ Simplified Init Pattern

Firebase ORM provides a convenient init() method that simplifies loading existing models:

// ✨ NEW: Simple one-liner to load existing data
const user = await User.init(userId);
if (user) {
  console.log(user.name);  // Data is already loaded!
}

// ✨ NEW: Load nested models with path parameters
const member = await Member.init(memberId, { website_id: websiteId });
if (member) {
  console.log(member.name);
}

// Traditional pattern (still supported)
const user = new User();
await user.load(userId);

// Traditional pattern for nested models (still supported)
const member = new Member();
member.setPathParams('website_id', websiteId);
await member.load(memberId);

// For creating new instances, use the constructor
const newUser = new User();
newUser.name = "Jane Doe";
newUser.email = "[email protected]";
await newUser.save();

Benefits of init():

  • 🎯 One line instead of two for loading data
  • 🔍 More intuitive - "initialize a User with this ID"
  • 🛡️ Returns null if not found (easier error handling)
  • 🗂️ Easy path parameters for nested collections
  • ✅ Fully compatible with existing code

🔄 Generic ORM Alias Functions

Firebase ORM now supports familiar ORM method names used in other popular frameworks, making it easier for developers coming from different ORMs:

// Static Methods (Class-level operations)
const users = await User.all();                    // Alias for getAll()
const user = await User.first('email', '==', '[email protected]'); // Alias for findOne()
const newUser = await User.create({               // Create and save in one step
  name: 'John Doe',
  email: '[email protected]'
});
await User.update('status', '==', 'pending', {status: 'active'}); // Update matching docs
await User.destroy('status', '==', 'inactive');   // Remove matching docs

// Instance Methods (Object-level operations)
const user = new User();
user.name = 'John Doe';
await user.create();                               // Alias for save()
await user.update({name: 'John Smith'});          // Update and save
await user.destroy();                             // Alias for remove()
await user.delete();                              // Alternative alias for remove()

Available Alias Methods

| Original Method | Alias Method | Description | |-----------------|--------------|-------------| | Model.getAll() | Model.all() | Get all documents | | Model.findOne() | Model.first() | Find first matching document | | N/A | Model.create(data) | Create and save new instance | | N/A | Model.update(field, op, value, data) | Update matching documents | | N/A | Model.destroy(field, op, value) | Remove matching documents | | instance.save() | instance.create() | Save instance (alias) | | N/A | instance.update(data?) | Update and save instance | | instance.remove() | instance.destroy() | Remove instance | | instance.remove() | instance.delete() | Remove instance (alternative) |

All original methods remain available for backward compatibility.

🏃‍♂️ Quick Start

  1. Install Firebase ORM

    npm install @arbel/firebase-orm firebase moment --save
  2. Initialize Firebase and configure global settings

    import { initializeApp } from 'firebase/app';
    import { getFirestore } from 'firebase/firestore';
    import { FirestoreOrmRepository } from '@arbel/firebase-orm';
    
    const app = initializeApp(firebaseConfig);
    const firestore = getFirestore(app);
    FirestoreOrmRepository.initGlobalConnection(firestore);
    
    // Configure global settings for automatic field naming and path_id generation
    FirestoreOrmRepository.setGlobalConfig({
      auto_lower_case_field_name: true, // cartItem → cart_item
      auto_path_id: true                // User class → user_id
    });
  3. Create your first model

    @Model({ reference_path: 'users' })  // path_id auto-generated as 'user_id'
    export class User extends BaseModel {
      @Field({ is_required: true })
      public firstName!: string;  // Stored as 'first_name' in database
    
      @Field({ is_required: true })
      public emailAddress!: string;  // Stored as 'email_address' in database
    }
  4. Start building!

    const user = new User();
    user.firstName = 'John';
    user.emailAddress = '[email protected]';
    await user.save();

👉 Continue with the full Quick Start Guide


The following examples show additional features from the original codebase: const allMembers = await Member.getAll();

//Get all members with age > 3 and weight > 30 const list = await Member.query().where('age','>','3').where('weight','>','30').get();

//Get all members with age > 3 or age < 3 limit 10 const list = await Member.query().where('age','>','3').orWhere('age','<','3').limit(10).get();

//Get the member tom const tom = await Member.findOne('firstName','==','Tom');

//Listen to changes in tom data in real time var unsubscribe = tom.on(()=>{ //Do something });

//Get all the list in real time var unsubscribe = Member.onList((member) => { //Do someting with the meber }) //Get all the list in real time when new meber is addedd var unsubscribe = Member.onList((member) => { //Do someting with the meber },LIST_EVENTS.ADDEDD) //Or var unsubscribe = Member.onModeList({

/**
 * Listen to add new objects from now
 */
added?: CallableFunction;

/**
 * Listen to removed objects
 */
removed? : CallableFunction

/**
 * Listen to modify objects
 */
modified? : CallableFunction

/**
 * Listen to init loading objects
 */
init? : CallableFunction

})

//Kill the listen process unsubscribe();


## 📋 Best Practices

### Naming Conventions

Firebase ORM works best with consistent naming conventions:

**✅ Recommended:**
- **Collection names**: Use lowercase with underscores (`users`, `user_profiles`, `shopping_carts`)
- **Field names**: Use camelCase in TypeScript with auto conversion enabled (`firstName`, `emailAddress`, `cartItems`)
- **Model classes**: Use PascalCase (`User`, `UserProfile`, `ShoppingCart`)

**❌ Avoid:**
- Mixed case collection names (`userProfiles`, `ShoppingCarts`)
- Snake_case in TypeScript properties (use camelCase instead)
- Inconsistent naming patterns

### Global Configuration Setup

For new projects, enable global configuration for consistency:

```typescript
FirestoreOrmRepository.setGlobalConfig({
  auto_lower_case_field_name: true,  // Automatic camelCase → snake_case conversion
  auto_path_id: true                 // Automatic path_id generation from class names
});

Model Structure

// ✅ Good: Clean, consistent structure with global config
@Model({
  reference_path: 'user_profiles'  // Lowercase with underscores
})
export class UserProfile extends BaseModel {
  @Field({ is_required: true })
  public firstName!: string;       // Auto-converted to first_name

  @Field({ is_required: true })
  public lastName!: string;        // Auto-converted to last_name

  @Field({ 
    is_required: false,
    field_name: 'avatar_url'       // Explicit naming when needed
  })
  public profileImage?: string;
}

👉 See complete Global Configuration guide

Installation

  1. Install the npm package:

    npm install @arbel/firebase-orm firebase rxfire moment --save

    For Firebase Admin SDK support:

    npm install @arbel/firebase-orm firebase-admin moment --save

TypeScript configuration

Also, make sure you are using TypeScript compiler version 3.3 or greater, and you have enabled the following settings in tsconfig.json:

"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"strictPropertyInitialization" : false,

You may also need to enable es6 in the lib section of compiler options, or install es6-shim from @types.

Module Format Support

The library supports both CommonJS (CJS) and ECMAScript Modules (ESM) formats:

  • For CommonJS environments (Node.js, older bundlers):

    const { FirestoreOrmRepository } = require("@arbel/firebase-orm");
  • For ESM environments (modern bundlers, TypeScript with ESM, Node.js with ESM):

    import { FirestoreOrmRepository } from "@arbel/firebase-orm";
Browser vs Node.js Entry Points

For Browser Applications (Angular, React, Vue, etc.):

Use the default entry point. This version excludes Admin SDK functionality and is optimized for browser bundles:

import { FirestoreOrmRepository } from "@arbel/firebase-orm";
import { initializeApp } from 'firebase/app';
import { getFirestore } from 'firebase/firestore';

const app = initializeApp(firebaseConfig);
const firestore = getFirestore(app);
FirestoreOrmRepository.initGlobalConnection(firestore);

For Node.js Applications with Firebase Admin SDK:

Use the /admin entry point for server-side functionality:

import { initializeAdminApp } from "@arbel/firebase-orm/admin";
import * as admin from 'firebase-admin';

const app = admin.initializeApp(adminConfig);
await initializeAdminApp(app);

Important: Admin SDK functionality has been moved to a separate entry point to prevent browser builds from including Node.js dependencies. See MIGRATION_GUIDE.md for migration details.

Quick Start

1.Create global connection

import * as app from "firebase";
import { FirestoreOrmRepository } from "@arbel/firebase-orm";

var firebaseApp = FirestoreOrmRepository.initializeApp(config);

Relationships

Firebase ORM provides comprehensive support for one-to-one, one-to-many, and many-to-many relationships through decorators. This allows you to model complex data relationships while maintaining the flexibility of Firestore's NoSQL structure.

Setting Up Relationships

Prerequisites

Before defining relationships, ensure you have:

  1. Imported the relationship decorators:
import { BelongsTo, HasOne, HasMany, BelongsToMany } from "@arbel/firebase-orm";
  1. Defined your models with proper @Model and @Field decorators
  2. Planned your database structure with clear foreign key patterns

Database Structure Considerations

When designing relationships in Firebase ORM:

  • Foreign Keys: Use string fields to store document IDs that reference other collections
  • Collection Naming: Use consistent naming patterns (e.g., users, posts, user_roles)
  • Path IDs: Ensure each model has a unique path_id field
  • Junction Tables: For many-to-many relationships, create separate models for junction tables

Relationship Types Overview

| Relationship | When to Use | Example | |-------------|-------------|---------| | @BelongsTo | This model has a foreign key pointing to another | User Profile → User | | @HasOne | Another model has a foreign key pointing to this | User → User Profile | | @HasMany | Another model has multiple records pointing to this | User → Posts | | @BelongsToMany | Many-to-many through junction table | User ↔ Roles |

One-to-One Relationships

One-to-one relationships connect exactly one record in one collection to exactly one record in another collection.

BelongsTo (this model has the foreign key)

Use @BelongsTo when this model stores a foreign key pointing to another model.

Database Structure:

users/
  user-1: { name: "John Doe" }
  
user_profiles/
  profile-1: { user_id: "user-1", bio: "Software developer" }

Setup:

import { Field, BaseModel, Model, BelongsTo } from "@arbel/firebase-orm";

// First, define the target model (User)
@Model({
  reference_path: 'users',
  path_id: 'user_id'
})
class User extends BaseModel {
  @Field({ is_required: true })
  public name!: string;
}

// Then, define the model with the foreign key (UserProfile)
@Model({
  reference_path: 'user_profiles',
  path_id: 'profile_id'
})
class UserProfile extends BaseModel {
  @Field({ is_required: true, field_name: 'user_id' })
  public userId!: string;  // This is the foreign key

  @Field({ is_required: false })
  public bio?: string;

  // Define the relationship
  @BelongsTo({
    model: User,           // The target model class
    localKey: 'userId'     // The local field containing the foreign key
  })
  public user?: User;      // Optional property to hold the loaded relationship
}

Usage:

// Load a profile and its related user
const profile = new UserProfile();
await profile.load('profile-1');

// Method 1: Load the relationship explicitly
const user = await profile.loadBelongsTo('user');
console.log(user.name); // "John Doe"

// Method 2: Access the loaded relationship (after loading)
await profile.loadWithRelationships(['user']);
console.log(profile.user?.name); // "John Doe"

HasOne (other model has the foreign key)

Use @HasOne when another model has a foreign key pointing to this model.

Database Structure:

users/
  user-1: { name: "John Doe" }
  
user_profiles/
  profile-1: { user_id: "user-1", bio: "Software developer" }

Setup:

@Model({
  reference_path: 'users',
  path_id: 'user_id'
})
class User extends BaseModel {
  @Field({ is_required: true })
  public name!: string;

  // Define the relationship
  @HasOne({
    model: UserProfile,     // The model that has the foreign key
    foreignKey: 'user_id'   // The field in UserProfile that references this User
  })
  public profile?: UserProfile;
}

Usage:

// Load a user and their profile
const user = new User();
await user.load('user-1');

// Load the related profile
const profile = await user.loadHasOne('profile');
console.log(profile.bio); // "Software developer"

One-to-Many Relationships

One-to-many relationships connect one record to multiple related records. This is common for parent-child relationships like User → Posts or Category → Products.

Database Structure:

users/
  user-1: { name: "John Doe" }
  user-2: { name: "Jane Smith" }

posts/
  post-1: { title: "First Post", author_id: "user-1" }
  post-2: { title: "Second Post", author_id: "user-1" }
  post-3: { title: "Jane's Post", author_id: "user-2" }

Setup:

// Define the "many" side (Post belongs to User)
@Model({
  reference_path: 'posts',
  path_id: 'post_id'
})
class Post extends BaseModel {
  @Field({ is_required: true })
  public title!: string;

  @Field({ is_required: true, field_name: 'author_id' })
  public authorId!: string;  // Foreign key to User

  // Each post belongs to one user
  @BelongsTo({
    model: User,
    localKey: 'authorId'
  })
  public author?: User;
}

// Define the "one" side (User has many Posts)
@Model({
  reference_path: 'users',
  path_id: 'user_id'
})
class User extends BaseModel {
  @Field({ is_required: true })
  public name!: string;

  // User has many posts
  @HasMany({
    model: Post,              // The model that contains the foreign key
    foreignKey: 'author_id'   // The field in Post that references this User
  })
  public posts?: Post[];
}

Usage:

// Load a user and their posts
const user = new User();
await user.load('user-1');

// Load all posts by this user
const posts = await user.loadHasMany('posts');
console.log(posts.length); // 2
console.log(posts[0].title); // "First Post"

// Load a post and its author
const post = new Post();
await post.load('post-1');

const author = await post.loadBelongsTo('author');
console.log(author.name); // "John Doe"

// Load both relationships at once
await user.loadWithRelationships(['posts']);
await post.loadWithRelationships(['author']);

Many-to-Many Relationships

Many-to-many relationships connect multiple records from one collection to multiple records in another collection. This requires a junction table (also called a pivot table) to store the relationships.

Database Structure:

users/
  user-1: { name: "John Doe" }
  user-2: { name: "Jane Smith" }

roles/
  role-1: { name: "Admin" }
  role-2: { name: "Editor" }
  role-3: { name: "Viewer" }

user_roles/ (junction table)
  ur-1: { user_id: "user-1", role_id: "role-1" }
  ur-2: { user_id: "user-1", role_id: "role-2" }
  ur-3: { user_id: "user-2", role_id: "role-3" }

Setup:

// Step 1: Define the junction table model
@Model({
  reference_path: 'user_roles',
  path_id: 'user_role_id'
})
class UserRole extends BaseModel {
  @Field({ is_required: true, field_name: 'user_id' })
  public userId!: string;

  @Field({ is_required: true, field_name: 'role_id' })
  public roleId!: string;

  // Optional: You can add relationships to the junction table too
  @BelongsTo({ model: User, localKey: 'userId' })
  public user?: User;

  @BelongsTo({ model: Role, localKey: 'roleId' })
  public role?: Role;
}

// Step 2: Define the Role model
@Model({
  reference_path: 'roles',
  path_id: 'role_id'
})
class Role extends BaseModel {
  @Field({ is_required: true })
  public name!: string;

  // Many-to-many: Role belongs to many users
  @BelongsToMany({
    model: User,           // The target model
    through: UserRole,     // The junction table model
    thisKey: 'role_id',    // Field in junction table that references this Role
    otherKey: 'user_id'    // Field in junction table that references the User
  })
  public users?: User[];
}

// Step 3: Define the User model
@Model({
  reference_path: 'users',
  path_id: 'user_id'
})
class User extends BaseModel {
  @Field({ is_required: true })
  public name!: string;

  // Many-to-many: User belongs to many roles
  @BelongsToMany({
    model: Role,           // The target model
    through: UserRole,     // The junction table model
    thisKey: 'user_id',    // Field in junction table that references this User
    otherKey: 'role_id'    // Field in junction table that references the Role
  })
  public roles?: Role[];
}

Usage:

// Load a user and their roles
const user = new User();
await user.load('user-1');

const roles = await user.loadBelongsToMany('roles');
console.log(roles.length); // 2
console.log(roles.map(r => r.name)); // ["Admin", "Editor"]

// Load a role and its users
const role = new Role();
await role.load('role-1');

const users = await role.loadBelongsToMany('users');
console.log(users.length); // 1
console.log(users[0].name); // "John Doe"

// Create new many-to-many relationships
const newUser = new User();
newUser.name = "Bob Wilson";
await newUser.save();

const adminRole = new Role();
await adminRole.load('role-1'); // Load existing Admin role

// Create the junction record
const userRole = new UserRole();
userRole.userId = newUser.getId();
userRole.roleId = adminRole.getId();
await userRole.save();

Advanced Relationship Loading

Loading Multiple Relationships

You can load all relationships at once using loadWithRelationships():

const user = new User();
await user.load('user-id');

// Load multiple relationships in one call
await user.loadWithRelationships(['profile', 'posts', 'roles']);

// Now you can access all loaded relationships
console.log(user.profile?.bio);
console.log(user.posts?.length);
console.log(user.roles?.map(r => r.name));

// Or load all relationships (if none specified, loads all defined relationships)
await user.loadWithRelationships();

Relationship Loading Methods Reference

| Method | Description | Returns | |--------|-------------|---------| | loadBelongsTo(name) | Load a single related model via foreign key | Promise<T & BaseModel> | | loadHasOne(name) | Load a single related model (reverse foreign key) | Promise<T & BaseModel> | | loadHasMany(name) | Load multiple related models | Promise<Array<T & BaseModel>> | | loadBelongsToMany(name) | Load many-to-many relationships | Promise<Array<T & BaseModel>> | | loadWithRelationships(names?) | Load multiple relationships at once | Promise<this> |

Error Handling

try {
  const user = new User();
  await user.load('user-id');
  
  const posts = await user.loadHasMany('posts');
  console.log(`User has ${posts.length} posts`);
} catch (error) {
  if (error.message.includes('Relationship not found')) {
    console.log('Relationship not defined on model');
  } else if (error.message.includes('not found')) {
    console.log('Related record not found');
  } else {
    console.error('Unexpected error:', error);
  }
}

Best Practices and Common Patterns

1. Consistent Naming Conventions

// Use consistent field naming patterns
@Field({ field_name: 'user_id' })    // Foreign key fields
public userId!: string;

@Field({ field_name: 'created_at' })  // Timestamp fields
public createdAt!: string;

// Use descriptive relationship names
@HasMany({ model: Post, foreignKey: 'author_id' })
public posts?: Post[];  // Clear what this represents

@BelongsTo({ model: User, localKey: 'authorId' })
public author?: User;   // Singular for one-to-one/many-to-one

2. Model Initialization Order

// Define models in dependency order to avoid circular references
// 1. Base models first (no dependencies)
class User extends BaseModel { /* ... */ }
class Role extends BaseModel { /* ... */ }

// 2. Junction tables next
class UserRole extends BaseModel { /* ... */ }

// 3. Models with relationships last
class Post extends BaseModel {
  @BelongsTo({ model: User, localKey: 'authorId' })
  public author?: User;
}

3. Performance Considerations

// Load relationships only when needed
const users = await User.getAll();

// Avoid N+1 queries - batch load relationships
for (const user of users) {
  await user.loadWithRelationships(['posts']); // Load all at once
}

// Or use specific loading methods
const activePosts = await user.loadHasMany('posts').then(posts => 
  posts.filter(p => p.status === 'active')
);

4. Data Integrity

// Always validate foreign keys before saving
class Post extends BaseModel {
  async save() {
    // Validate author exists before saving
    if (this.authorId) {
      const author = new User();
      try {
        await author.load(this.authorId);
      } catch (error) {
        throw new Error(`Invalid author_id: ${this.authorId}`);
      }
    }
    return super.save();
  }
}

Troubleshooting Common Issues

Issue: "Relationship not found"

// ❌ Wrong: Relationship name doesn't match decorator property
@HasMany({ model: Post, foreignKey: 'author_id' })
public userPosts?: Post[];  // Property name is 'userPosts'

await user.loadHasMany('posts'); // ❌ Looking for 'posts' but property is 'userPosts'

// ✅ Correct: Relationship name matches property name
await user.loadHasMany('userPosts'); // ✅ Matches property name

Issue: "No records found"

// Check if the foreign key relationship is correct
const profile = new UserProfile();
profile.userId = 'wrong-user-id'; // ❌ Non-existent user ID

try {
  const user = await profile.loadBelongsTo('user');
} catch (error) {
  console.log('User not found for profile'); // Handle gracefully
}

Issue: Circular Dependencies

// ❌ Avoid circular imports - define models in separate files
// user.model.ts
import { Post } from './post.model'; // ❌ Circular if Post imports User

// ✅ Use forward references or organize dependencies properly
// base-models.ts - Define all base models
// relationship-models.ts - Add relationships after base definitions

Complete Example: Blog System

Here's a complete example showing how to set up a blog system with relationships:

import { Field, BaseModel, Model, BelongsTo, HasOne, HasMany, BelongsToMany } from "@arbel/firebase-orm";

// 1. Base Models (no dependencies)
@Model({
  reference_path: 'users',
  path_id: 'user_id'
})
export class User extends BaseModel {
  @Field({ is_required: true })
  public name!: string;

  @Field({ is_required: true })
  public email!: string;

  @Field({ is_required: false, field_name: 'created_at' })
  public createdAt?: string;

  // Relationships
  @HasOne({ model: UserProfile, foreignKey: 'user_id' })
  public profile?: UserProfile;

  @HasMany({ model: Post, foreignKey: 'author_id' })
  public posts?: Post[];

  @HasMany({ model: Comment, foreignKey: 'user_id' })
  public comments?: Comment[];

  @BelongsToMany({
    model: Tag,
    through: UserTag,
    thisKey: 'user_id',
    otherKey: 'tag_id'
  })
  public followedTags?: Tag[];
}

@Model({
  reference_path: 'tags',
  path_id: 'tag_id'
})
export class Tag extends BaseModel {
  @Field({ is_required: true })
  public name!: string;

  @Field({ is_required: false })
  public description?: string;

  // Relationships
  @BelongsToMany({
    model: Post,
    through: PostTag,
    thisKey: 'tag_id',
    otherKey: 'post_id'
  })
  public posts?: Post[];

  @BelongsToMany({
    model: User,
    through: UserTag,
    thisKey: 'tag_id',
    otherKey: 'user_id'
  })
  public followers?: User[];
}

// 2. Junction Tables
@Model({
  reference_path: 'post_tags',
  path_id: 'post_tag_id'
})
export class PostTag extends BaseModel {
  @Field({ is_required: true, field_name: 'post_id' })
  public postId!: string;

  @Field({ is_required: true, field_name: 'tag_id' })
  public tagId!: string;
}

@Model({
  reference_path: 'user_tags',
  path_id: 'user_tag_id'
})
export class UserTag extends BaseModel {
  @Field({ is_required: true, field_name: 'user_id' })
  public userId!: string;

  @Field({ is_required: true, field_name: 'tag_id' })
  public tagId!: string;
}

// 3. Dependent Models
@Model({
  reference_path: 'user_profiles',
  path_id: 'profile_id'
})
export class UserProfile extends BaseModel {
  @Field({ is_required: true, field_name: 'user_id' })
  public userId!: string;

  @Field({ is_required: false })
  public bio?: string;

  @Field({ is_required: false, field_name: 'avatar_url' })
  public avatarUrl?: string;

  @Field({ is_required: false })
  public website?: string;

  // Relationships
  @BelongsTo({ model: User, localKey: 'userId' })
  public user?: User;
}

@Model({
  reference_path: 'posts',
  path_id: 'post_id'
})
export class Post extends BaseModel {
  @Field({ is_required: true })
  public title!: string;

  @Field({ is_required: true })
  public content!: string;

  @Field({ is_required: true, field_name: 'author_id' })
  public authorId!: string;

  @Field({ is_required: false, field_name: 'published_at' })
  public publishedAt?: string;

  @Field({ is_required: false })
  public status?: 'draft' | 'published' | 'archived';

  // Relationships
  @BelongsTo({ model: User, localKey: 'authorId' })
  public author?: User;

  @HasMany({ model: Comment, foreignKey: 'post_id' })
  public comments?: Comment[];

  @BelongsToMany({
    model: Tag,
    through: PostTag,
    thisKey: 'post_id',
    otherKey: 'tag_id'
  })
  public tags?: Tag[];
}

@Model({
  reference_path: 'comments',
  path_id: 'comment_id'
})
export class Comment extends BaseModel {
  @Field({ is_required: true })
  public content!: string;

  @Field({ is_required: true, field_name: 'post_id' })
  public postId!: string;

  @Field({ is_required: true, field_name: 'user_id' })
  public userId!: string;

  @Field({ is_required: false, field_name: 'parent_id' })
  public parentId?: string; // For nested comments

  @Field({ is_required: false, field_name: 'created_at' })
  public createdAt?: string;

  // Relationships
  @BelongsTo({ model: Post, localKey: 'postId' })
  public post?: Post;

  @BelongsTo({ model: User, localKey: 'userId' })
  public user?: User;

  @BelongsTo({ model: Comment, localKey: 'parentId' })
  public parent?: Comment;

  @HasMany({ model: Comment, foreignKey: 'parent_id' })
  public replies?: Comment[];
}

// Usage Examples:

// Create a new blog post with relationships
async function createBlogPost() {
  // 1. Create or load the author
  const author = new User();
  author.name = "John Doe";
  author.email = "[email protected]";
  author.createdAt = new Date().toISOString();
  await author.save();

  // 2. Create the post
  const post = new Post();
  post.title = "Getting Started with Firebase ORM";
  post.content = "This is a comprehensive guide...";
  post.authorId = author.getId();
  post.status = 'published';
  post.publishedAt = new Date().toISOString();
  await post.save();

  // 3. Create some tags
  const jsTag = new Tag();
  jsTag.name = "JavaScript";
  jsTag.description = "JavaScript programming language";
  await jsTag.save();

  const ormTag = new Tag();
  ormTag.name = "ORM";
  ormTag.description = "Object-Relational Mapping";
  await ormTag.save();

  // 4. Associate tags with the post
  const postTag1 = new PostTag();
  postTag1.postId = post.getId();
  postTag1.tagId = jsTag.getId();
  await postTag1.save();

  const postTag2 = new PostTag();
  postTag2.postId = post.getId();
  postTag2.tagId = ormTag.getId();
  await postTag2.save();

  return { post, author, tags: [jsTag, ormTag] };
}

// Load a complete blog post with all relationships
async function loadBlogPostWithRelationships(postId: string) {
  const post = new Post();
  await post.load(postId);

  // Load all relationships
  await post.loadWithRelationships(['author', 'comments', 'tags']);

  // Access the loaded data
  console.log(`Post: ${post.title}`);
  console.log(`Author: ${post.author?.name}`);
  console.log(`Tags: ${post.tags?.map(t => t.name).join(', ')}`);
  console.log(`Comments: ${post.comments?.length} comments`);

  // Load nested relationships for comments
  if (post.comments) {
    for (const comment of post.comments) {
      await comment.loadWithRelationships(['user', 'replies']);
      console.log(`Comment by ${comment.user?.name}: ${comment.content}`);
      
      if (comment.replies?.length) {
        for (const reply of comment.replies) {
          await reply.loadBelongsTo('user');
          console.log(`  Reply by ${reply.user?.name}: ${reply.content}`);
        }
      }
    }
  }

  return post;
}

// Find posts by tag
async function findPostsByTag(tagName: string) {
  // Find the tag
  const tag = await Tag.findOne('name', '==', tagName);
  if (!tag) {
    throw new Error(`Tag '${tagName}' not found`);
  }

  // Load posts with this tag
  const posts = await tag.loadBelongsToMany('posts');
  
  // Load authors for all posts
  for (const post of posts) {
    await post.loadBelongsTo('author');
  }

  return posts;
}

// Get user's blog activity
async function getUserBlogActivity(userId: string) {
  const user = new User();
  await user.load(userId);

  // Load all user's blog-related data
  await user.loadWithRelationships(['profile', 'posts', 'comments', 'followedTags']);

  const activity = {
    user: {
      name: user.name,
      email: user.email,
      bio: user.profile?.bio,
      avatar: user.profile?.avatarUrl
    },
    stats: {
      postsCount: user.posts?.length || 0,
      commentsCount: user.comments?.length || 0,
      followedTagsCount: user.followedTags?.length || 0
    },
    recentPosts: user.posts?.slice(0, 5).map(p => ({
      title: p.title,
      status: p.status,
      publishedAt: p.publishedAt
    })),
    followedTags: user.followedTags?.map(t => t.name)
  };

  return activity;
}

Legacy Relationship Methods

For backward compatibility, the original relationship methods are still available:

// Load one related model (assumes foreign key pattern)
const relatedModel = await model.getOneRel(RelatedModel);

// Load many related models (assumes foreign key pattern)  
const relatedModels = await model.getManyRel(RelatedModel);

Note: These legacy methods use automatic foreign key detection based on model names and may not work reliably with complex relationships. The new decorator-based approach is recommended for all new projects.

1.Initialize with Firebase Admin SDK

import * as admin from "firebase-admin";
import { FirestoreOrmRepository } from "@arbel/firebase-orm";

// Initialize Firebase Admin with your credentials
const adminApp = admin.initializeApp({
  credential: admin.credential.cert(serviceAccount),
  databaseURL: "https://your-project-id.firebaseio.com",
  storageBucket: "your-project-id.appspot.com"
});

// Initialize Firebase ORM with the Admin app
FirestoreOrmRepository.initializeAdminApp(adminApp);

// Initialize storage (optional)
const adminStorage = admin.storage();
FirestoreOrmRepository.initGlobalStorage(adminStorage);

Usage with Firebase Admin

1.Initialize with Firebase Admin SDK

import * as admin from "firebase-admin";
import { FirestoreOrmRepository } from "@arbel/firebase-orm";

// Initialize Firebase Admin with your credentials
const adminApp = admin.initializeApp({
  credential: admin.credential.cert(serviceAccount),
  databaseURL: "https://your-project-id.firebaseio.com",
  storageBucket: "your-project-id.appspot.com"
});

// Initialize Firebase ORM with the Admin app
FirestoreOrmRepository.initializeAdminApp(adminApp);

// Initialize storage (optional)
const adminStorage = admin.storage();
FirestoreOrmRepository.initGlobalStorage(adminStorage);

2.Create global path id - (optional)

import { FirestoreOrmRepository } from "@arbel/firebase-orm";

FirestoreOrmRepository.initGlobalPath("user_id", 50);

3.Create new object

import { Member } from "model/member";

const member = new Member();
member.name = "Timber";
member.photoUrl = "https://www.example.com/image.png";
member.save();

Database Structure

  • only varibales with the decorator @Field will save in the database
  • every model must to include path_id attribute that need to be unique
  • reference_path is the path of the model data inside the dataabse

Text indexing / LIKE Search

1.Add the flag is_text_indexing to @Field decorator

import { Field, BaseModel, Model } from "@arbel/firebase-orm";

@Model({
  reference_path: "websites/:website_id/members",
  path_id: "member_id"
})
export class Member extends BaseModel {
  @Field({
    is_text_indexing: true
  })
  public name!: string;

  @Field({
    is_required: true
  })
  public age!: number;

  @Field({
    is_required: true
  })
  public weight!: number;
}
  1. save new value inside the variable.
  2. use like operator as you need
//Get all members with age > 3 and weight > 30 and name conatin `Dav`
const list = await Member.query()
  .where("age", ">", "3")
  .where("weight", ">", "30")
  .like("name", "%Dav%")
  .get();

Elasticsearch support

1.Add firebase function with onWrite trigger

import * as functions from "firebase-functions";
import { Client } from "@elastic/elasticsearch";

const client = new Client({
  cloud: {
    id: "xxxxxxx",
    username: "xxxxx",
    password: "xxxxxxx"
  }
});

export const elasticsearchProductsSync = functions.firestore
  .document("products/{productId}")
  .onWrite((snap, context) => {
    const productId = context.params.productId;
    const newData = snap.after.data();
    // ...or the previous value before this update
    const previousData = snap.before.data();

    if (newData) {
      newData.id = productId;

      if (!previousData) {
        printLog("create new product - ", productId);
        client
          .create({
            index: "products",
            type: "_doc",
            id: productId,
            body: newData
          })
          .catch(e => {
            var error =
              e.meta && e.meta.body && e.meta.body.error ? e.meta.body : e;
            console.error("Elasticsearch error - ", error);
          });
      } else {
        printLog("update product - ", productId);
        client.transport
          .request({
            method: "POST",
            path: "/products/_doc/" + productId,
            body: newData
          })
          .catch(e => {
            var error =
              e.meta && e.meta.body && e.meta.body.error ? e.meta.body : e;
            console.error("Elasticsearch error - ", error);
          });
      }
    } else {
      printLog("delete product - ", productId);
      client
        .delete({
          index: "products",
          type: "_doc",
          id: productId
        })
        .catch(e => {
          var error =
            e.meta && e.meta.body && e.meta.body.error ? e.meta.body : e;
          console.error("Elasticsearch error - ", error);
        });
    }

    return true;
  });
  1. set global elasticsearch url
FirestoreOrmRepository.initGlobalElasticsearchConnection(
  "https://elasticsearch.com"
);
  1. fetch the data as sql
var result: any = await Product.elasticSql("WHERE qty > 0", 3);
//Total rows
var totalCount = await result.count();
var current = 0;
//Pagination
while (result.next) {
  index++;
  var result = await result.next();
}
  1. or to use binding sql
var result: any = await Product.elasticSql([
  "WHERE name in (:options)  and cost > :cost",
  {
    options: ["a", "b", "c"],
    cost: 9
  }
]);
//Total rows
var totalCount = await result.count();
var current = 0;
//Pagination
while (result.next) {
  index++;
  var result = await result.next();
}

Firebase Storage support

1.Initilize firebase storage connection

var firebaseApp: any = firebase.initializeApp(config.api.firebase);
var storage = firebaseApp.storage();
FirestoreOrmRepository.initGlobalStorage(storage);
  1. Get the storage reference of the wanted field
var product = new Product();
product.name = "test product";
var storageRef = product.getStorageFile("photoUrl");
  1. Upload file
var product = new Product();
await product.getStorageFile("photoUrl").uploadFile(file);
product.save();
  1. Upload file from string
var product = new Product();
await product.getStorageFile("photoUrl").uploadString(file,'base64');
product.save();
  1. Upload file from url (copy file to storage)
var product = new Product();
await product.getStorageFile("photoUrl").uploadFromUrl(url);
product.save();
  1. Get file firebase storage ref
var product = new Product();
var ref = product.getStorageFile("photoUrl").getRef();
  1. Track progress
var product = new Product();
product.name = "test product";
var storageRef = product.getStorageFile("photoUrl");
await storageRef.uploadFromUrl(
  "https://img.example.com/image.jpg",
  function(snapshot: any) {
    // Observe state change events such as progress, pause, and resume
    // Get task progress, including the number of bytes uploaded and the total number of bytes to be uploaded
    var progress = (snapshot.bytesTransferred / snapshot.totalBytes) * 100;
    switch (snapshot.state) {
      case firebase.storage.TaskState.PAUSED: // or 'paused'
        break;
      case firebase.storage.TaskState.RUNNING: // or 'running'
        break;
    }
  },
  function(error: any) {
    // Handle unsuccessful uploads
  },
  function(task: any) {
    // Handle successful uploads on complete
    // For instance, get the download URL: https://firebasestorage.googleapis.com/...
    task.snapshot.ref.getDownloadURL().then(function(downloadURL: any) {
      printLog("File available at", downloadURL);
    });
  }
);
await product.save();