Skip to the content.

Firebase Functions Integration

Firebase ORM works seamlessly with Firebase Functions, enabling you to build powerful server-side applications with the same elegant patterns you use on the client side.

Why Use Firebase ORM with Functions?

Setup

1. Install Dependencies

npm install @arbel/firebase-orm firebase-admin firebase-functions

2. Initialize Firebase ORM in Functions

// functions/src/index.ts
import * as functions from 'firebase-functions';
import * as admin from 'firebase-admin';
import { FirestoreOrmRepository } from '@arbel/firebase-orm';

// Initialize Firebase Admin
admin.initializeApp();

// Initialize Firebase ORM with admin SDK
FirestoreOrmRepository.initGlobalConnection(admin.firestore(), 'admin');

// Import your models
import './models/User';
import './models/Product';
import './models/Order';

3. Model Definitions

Use the same model definitions in your functions:

// functions/src/models/Product.ts
import { BaseModel, Model, Field } from '@arbel/firebase-orm';

@Model({
  reference_path: 'products',
  path_id: 'product_id'
})
export class Product extends BaseModel {
  @Field({ is_required: true })
  public name!: string;

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

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

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

  @Field({ 
    is_text_indexing: true,  // Enable for search functionality
    is_required: false 
  })
  public searchKeywords?: string;

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

  @Field({ field_name: 'updated_at' })
  public updatedAt?: string;
}

Database Triggers

Use Firebase ORM in database triggers to respond to data changes:

Product Processing Trigger

// functions/src/triggers/productTriggers.ts
import * as functions from 'firebase-functions';
import { Product } from '../models/Product';

export const processProductChanges = functions.firestore
  .document('products/{productId}')
  .onWrite(async (change, context) => {
    const productId = context.params.productId;
    const after = change.after.data();
    const before = change.before.data();

    if (!after) {
      // Product was deleted
      console.log(`Product ${productId} was deleted`);
      await handleProductDeletion(productId, before);
      return;
    }

    if (!before) {
      // New product created
      console.log(`New product ${productId} created`);
      await handleNewProduct(productId, after);
      return;
    }

    // Product was updated
    console.log(`Product ${productId} updated`);
    await handleProductUpdate(productId, before, after);
  });

async function handleNewProduct(productId: string, data: any) {
  const product = new Product();
  await product.load(productId);

  // Generate search keywords
  const keywords = generateSearchKeywords(product.name, product.description, product.category);
  product.searchKeywords = keywords.join(' ');

  // Set timestamps
  product.createdAt = new Date().toISOString();
  product.updatedAt = new Date().toISOString();

  await product.save();

  // Trigger additional processing
  await updateInventory(product);
  await notifyAdmins('New product added', product);
}

async function handleProductUpdate(productId: string, before: any, after: any) {
  const product = new Product();
  await product.load(productId);

  // Update search keywords if content changed
  if (before.name !== after.name || before.description !== after.description) {
    const keywords = generateSearchKeywords(product.name, product.description, product.category);
    product.searchKeywords = keywords.join(' ');
  }

  // Update timestamp
  product.updatedAt = new Date().toISOString();

  await product.save();
}

function generateSearchKeywords(name: string, description?: string, category?: string): string[] {
  const keywords = new Set<string>();
  
  // Add name words
  name.toLowerCase().split(/\s+/).forEach(word => {
    if (word.length > 2) keywords.add(word);
  });

  // Add description words
  if (description) {
    description.toLowerCase().split(/\s+/).forEach(word => {
      if (word.length > 2) keywords.add(word);
    });
  }

  // Add category
  if (category) {
    keywords.add(category.toLowerCase());
  }

  return Array.from(keywords);
}

HTTP Functions

Create REST APIs using Firebase ORM:

Product API

// functions/src/api/productApi.ts
import * as functions from 'firebase-functions';
import * as express from 'express';
import * as cors from 'cors';
import { Product } from '../models/Product';

const app = express();
app.use(cors({ origin: true }));
app.use(express.json());

// Get all products with pagination
app.get('/products', async (req, res) => {
  try {
    const page = parseInt(req.query.page as string) || 1;
    const limit = parseInt(req.query.limit as string) || 10;
    const category = req.query.category as string;

    let query = Product.query()
      .orderBy('createdAt', 'desc')
      .limit(limit);

    if (category) {
      query = query.where('category', '==', category);
    }

    // Implement pagination (simplified)
    if (page > 1) {
      const skip = (page - 1) * limit;
      // Note: Firestore doesn't have skip, use cursor-based pagination in production
    }

    const products = await query.get();
    
    res.json({
      products: products.map(p => p.toJson()),
      page,
      limit,
      total: await Product.count()
    });
  } catch (error) {
    console.error('Error fetching products:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

// Get single product
app.get('/products/:id', async (req, res) => {
  try {
    const product = new Product();
    await product.load(req.params.id);
    
    res.json(product.toJson());
  } catch (error) {
    if (error.message.includes('not found')) {
      res.status(404).json({ error: 'Product not found' });
    } else {
      console.error('Error fetching product:', error);
      res.status(500).json({ error: 'Internal server error' });
    }
  }
});

// Create new product
app.post('/products', async (req, res) => {
  try {
    const { name, price, description, category } = req.body;

    // Validation
    if (!name || !price || !category) {
      return res.status(400).json({ 
        error: 'Missing required fields: name, price, category' 
      });
    }

    const product = new Product();
    product.name = name;
    product.price = price;
    product.description = description;
    product.category = category;
    product.createdAt = new Date().toISOString();
    product.updatedAt = new Date().toISOString();

    await product.save();

    res.status(201).json(product.toJson());
  } catch (error) {
    console.error('Error creating product:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

// Update product
app.put('/products/:id', async (req, res) => {
  try {
    const product = new Product();
    await product.load(req.params.id);

    const { name, price, description, category } = req.body;

    if (name !== undefined) product.name = name;
    if (price !== undefined) product.price = price;
    if (description !== undefined) product.description = description;
    if (category !== undefined) product.category = category;
    
    product.updatedAt = new Date().toISOString();

    await product.save();

    res.json(product.toJson());
  } catch (error) {
    if (error.message.includes('not found')) {
      res.status(404).json({ error: 'Product not found' });
    } else {
      console.error('Error updating product:', error);
      res.status(500).json({ error: 'Internal server error' });
    }
  }
});

// Delete product
app.delete('/products/:id', async (req, res) => {
  try {
    const product = new Product();
    await product.load(req.params.id);
    await product.destroy();

    res.status(204).send();
  } catch (error) {
    if (error.message.includes('not found')) {
      res.status(404).json({ error: 'Product not found' });
    } else {
      console.error('Error deleting product:', error);
      res.status(500).json({ error: 'Internal server error' });
    }
  }
});

// Search products
app.get('/products/search/:query', async (req, res) => {
  try {
    const searchQuery = req.params.query.toLowerCase();
    
    const products = await Product.query()
      .like('searchKeywords', `%${searchQuery}%`)
      .limit(20)
      .get();

    res.json({
      query: searchQuery,
      results: products.map(p => p.toJson())
    });
  } catch (error) {
    console.error('Error searching products:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

export const productApi = functions.https.onRequest(app);

Elasticsearch Integration

Integrate with Elasticsearch for advanced search capabilities:

// functions/src/elasticsearch/sync.ts
import * as functions from 'firebase-functions';
import { Client } from '@elastic/elasticsearch';
import { Product } from '../models/Product';

const client = new Client({
  cloud: {
    id: functions.config().elasticsearch.cloud_id,
  },
  auth: {
    username: functions.config().elasticsearch.username,
    password: functions.config().elasticsearch.password,
  }
});

export const elasticsearchProductsSync = functions.firestore
  .document('products/{productId}')
  .onWrite(async (change, context) => {
    const productId = context.params.productId;
    const newData = change.after.data();
    const previousData = change.before.data();

    try {
      if (newData) {
        // Document created or updated
        const product = new Product();
        await product.load(productId);

        const esDocument = {
          id: productId,
          name: product.name,
          description: product.description,
          category: product.category,
          price: product.price,
          searchKeywords: product.searchKeywords,
          createdAt: product.createdAt,
          updatedAt: product.updatedAt,
        };

        if (!previousData) {
          // Create new document
          console.log('Creating new product in Elasticsearch:', productId);
          await client.create({
            index: 'products',
            id: productId,
            body: esDocument
          });
        } else {
          // Update existing document
          console.log('Updating product in Elasticsearch:', productId);
          await client.update({
            index: 'products',
            id: productId,
            body: {
              doc: esDocument
            }
          });
        }
      } else {
        // Document deleted
        console.log('Deleting product from Elasticsearch:', productId);
        await client.delete({
          index: 'products',
          id: productId
        });
      }
    } catch (error) {
      console.error('Elasticsearch sync error:', error);
      
      // Log error but don't fail the function
      // You might want to add the failed sync to a retry queue
    }
  });

// Advanced search function
export const searchProducts = functions.https.onCall(async (data, context) => {
  try {
    const { query, category, minPrice, maxPrice, limit = 10 } = data;

    const searchBody: any = {
      query: {
        bool: {
          must: [],
          filter: []
        }
      },
      size: limit,
      sort: [{ _score: { order: 'desc' } }]
    };

    // Text search
    if (query) {
      searchBody.query.bool.must.push({
        multi_match: {
          query,
          fields: ['name^2', 'description', 'searchKeywords'],
          type: 'best_fields',
          fuzziness: 'AUTO'
        }
      });
    }

    // Category filter
    if (category) {
      searchBody.query.bool.filter.push({
        term: { category: category.toLowerCase() }
      });
    }

    // Price range filter
    if (minPrice !== undefined || maxPrice !== undefined) {
      const priceFilter: any = { range: { price: {} } };
      if (minPrice !== undefined) priceFilter.range.price.gte = minPrice;
      if (maxPrice !== undefined) priceFilter.range.price.lte = maxPrice;
      searchBody.query.bool.filter.push(priceFilter);
    }

    const response = await client.search({
      index: 'products',
      body: searchBody
    });

    return {
      hits: response.body.hits.hits.map((hit: any) => ({
        id: hit._id,
        score: hit._score,
        ...hit._source
      })),
      total: response.body.hits.total.value
    };
  } catch (error) {
    console.error('Search error:', error);
    throw new functions.https.HttpsError('internal', 'Search failed');
  }
});

Scheduled Functions

Use Firebase ORM in scheduled functions for background processing:

// functions/src/scheduled/maintenance.ts
import * as functions from 'firebase-functions';
import { Product } from '../models/Product';

// Run daily cleanup at 2 AM
export const dailyCleanup = functions.pubsub
  .schedule('0 2 * * *')
  .timeZone('America/New_York')
  .onRun(async (context) => {
    console.log('Running daily cleanup...');

    try {
      // Clean up old products marked for deletion
      const productsToDelete = await Product.query()
        .where('deletedAt', '!=', null)
        .where('deletedAt', '<', getDateDaysAgo(30))
        .get();

      console.log(`Found ${productsToDelete.length} products to permanently delete`);

      for (const product of productsToDelete) {
        await product.destroy();
        console.log(`Permanently deleted product: ${product.getId()}`);
      }

      // Update product statistics
      await updateProductStatistics();

      console.log('Daily cleanup completed successfully');
    } catch (error) {
      console.error('Daily cleanup failed:', error);
    }
  });

// Generate weekly reports
export const weeklyReports = functions.pubsub
  .schedule('0 9 * * MON')
  .timeZone('America/New_York')
  .onRun(async (context) => {
    console.log('Generating weekly reports...');

    try {
      // Get product statistics
      const totalProducts = await Product.count();
      const newProductsThisWeek = await Product.query()
        .where('createdAt', '>=', getDateDaysAgo(7))
        .get();

      const categoryCounts = await getCategoryStatistics();

      const report = {
        period: 'weekly',
        generatedAt: new Date().toISOString(),
        totalProducts,
        newProductsThisWeek: newProductsThisWeek.length,
        categoryCounts,
      };

      // Save report (you could send email, save to database, etc.)
      console.log('Weekly report:', report);

      // Send to admin dashboard
      await saveReport(report);

    } catch (error) {
      console.error('Weekly report generation failed:', error);
    }
  });

function getDateDaysAgo(days: number): string {
  const date = new Date();
  date.setDate(date.getDate() - days);
  return date.toISOString();
}

async function getCategoryStatistics() {
  const categories = ['electronics', 'clothing', 'books', 'home'];
  const counts: Record<string, number> = {};

  for (const category of categories) {
    const products = await Product.query()
      .where('category', '==', category)
      .get();
    counts[category] = products.length;
  }

  return counts;
}

async function saveReport(report: any) {
  // Implementation depends on where you want to save reports
  // Could be Firestore, Cloud Storage, email, etc.
  console.log('Saving report:', report);
}

async function updateProductStatistics() {
  // Update cached statistics for dashboard
  const stats = {
    totalProducts: await Product.count(),
    lastUpdated: new Date().toISOString(),
  };

  // Save to a statistics document
  console.log('Updated statistics:', stats);
}

Authentication Middleware

Add authentication middleware for secure API endpoints:

// functions/src/middleware/auth.ts
import * as admin from 'firebase-admin';
import { Request, Response, NextFunction } from 'express';

export interface AuthenticatedRequest extends Request {
  user?: admin.auth.DecodedIdToken;
}

export const authenticateUser = async (
  req: AuthenticatedRequest, 
  res: Response, 
  next: NextFunction
) => {
  try {
    const token = req.headers.authorization?.replace('Bearer ', '');
    
    if (!token) {
      return res.status(401).json({ error: 'No authentication token provided' });
    }

    const decodedToken = await admin.auth().verifyIdToken(token);
    req.user = decodedToken;
    
    next();
  } catch (error) {
    console.error('Authentication error:', error);
    res.status(401).json({ error: 'Invalid authentication token' });
  }
};

export const requireAdmin = (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
  if (!req.user) {
    return res.status(401).json({ error: 'Authentication required' });
  }

  // Check if user has admin role (implement based on your user model)
  if (!req.user.admin) {
    return res.status(403).json({ error: 'Admin access required' });
  }

  next();
};

Best Practices

1. Error Handling

export const safeFunction = functions.https.onCall(async (data, context) => {
  try {
    // Your Firebase ORM operations
    const result = await Product.getAll();
    return { success: true, data: result };
  } catch (error) {
    console.error('Function error:', error);
    
    // Return user-friendly errors
    if (error.message.includes('permission-denied')) {
      throw new functions.https.HttpsError('permission-denied', 'Access denied');
    }
    
    throw new functions.https.HttpsError('internal', 'Internal server error');
  }
});

2. Performance Optimization

// Batch operations for better performance
export const batchUpdateProducts = functions.https.onCall(async (data, context) => {
  const { productIds, updates } = data;
  
  // Load all products in parallel
  const products = await Promise.all(
    productIds.map(async (id: string) => {
      const product = new Product();
      await product.load(id);
      return product;
    })
  );

  // Update all products
  products.forEach(product => {
    product.initFromData(updates);
    product.updatedAt = new Date().toISOString();
  });

  // Save all products in parallel
  await Promise.all(products.map(product => product.save()));

  return { updated: products.length };
});

3. Monitoring and Logging

import * as functions from 'firebase-functions';

export const monitoredFunction = functions.https.onCall(async (data, context) => {
  const startTime = Date.now();
  
  try {
    console.log('Function started', { data, user: context.auth?.uid });
    
    // Your operation
    const result = await performOperation(data);
    
    const duration = Date.now() - startTime;
    console.log('Function completed', { duration, resultCount: result.length });
    
    return result;
  } catch (error) {
    const duration = Date.now() - startTime;
    console.error('Function failed', { error: error.message, duration });
    throw error;
  }
});

Deployment

Deploy your functions with Firebase ORM:

# Install dependencies
cd functions
npm install

# Deploy all functions
firebase deploy --only functions

# Deploy specific function
firebase deploy --only functions:productApi

# Deploy with environment variables
firebase functions:config:set elasticsearch.cloud_id="your-cloud-id"
firebase functions:config:set elasticsearch.username="your-username" 
firebase functions:config:set elasticsearch.password="your-password"

Firebase Functions with Firebase ORM provides a powerful combination for building scalable, maintainable server-side applications with consistent patterns across your entire stack.