Saltar a contenido

@cyber-eco/services

The service layer for the CyberEco ecosystem. Provides DataLayerService (the core data orchestrator), domain-specific services, caching, sync, webhooks, and the createDataLayer() factory for bootstrapping.

Installation

npm install @cyber-eco/services
Dependency Purpose
@cyber-eco/types Interfaces and constants
@cyber-eco/auth Permission evaluation

Critical Constraint: Zero Firebase Imports

@cyber-eco/services must have zero imports from firebase/*. All data access goes through the StorageAdapter interface. This constraint enables:

  • Testing without Firebase emulators
  • Future migration to IPFS, blockchain, or any other backend
  • Clean separation of concerns

Verification:

grep -r "firebase" packages/services/src/
# Must return zero results

The createDataLayer() Factory

The recommended way to bootstrap the entire data layer. Solves the circular dependency between DataLayerService and PermissionService.

import { createDataLayer } from '@cyber-eco/services';
import type { CyberEcoDataLayer } from '@cyber-eco/services';
import { FirebaseStorageAdapter, getHubFirestore } from '@cyber-eco/firebase';

// 1. Create the storage adapter
const adapter = new FirebaseStorageAdapter(() => getHubFirestore());

// 2. Bootstrap the full data layer
const dataLayer: CyberEcoDataLayer = createDataLayer({
  adapter,
  cache: { strategy: 'lru', maxSize: 500, defaultTtl: 300 },
  permissions: { enabled: true },
  sync: { enabled: true },
  webhooks: { enabled: false },
});

// 3. Use the services
const user = await dataLayer.core.get('requester-id', 'users', 'user-123');
await dataLayer.notifications.getForUser('user-123');
await dataLayer.permissions.evaluatePermission('user-123', 'users', 'read');

CyberEcoDataLayer Shape

interface CyberEcoDataLayer {
  core: DataLayerService;           // Central data orchestrator
  permissions: PermissionService;    // Permission evaluation
  sharedData: SharedDataService;     // Cross-app shared data
  notifications: NotificationService; // User notifications
  dashboard: DashboardService;       // Dashboard aggregations
  dataExport: DataExportService;     // Data export (GDPR, backups)
}

How the Factory Resolves Circular Dependencies

DataLayerService needs PermissionService to check permissions before data access. PermissionService needs DataLayerService to read permission records from the database. The factory resolves this in three steps:

function createDataLayer(config: DataLayerConfig): CyberEcoDataLayer {
  // Step 1: Create DataLayerService WITHOUT permission checking
  const core = new DataLayerService(config);

  // Step 2: Create PermissionService that uses core for data access
  const permissions = new PermissionService(core);

  // Step 3: Wire permission checking BACK into core
  core.setPermissionChecker(permissions.evaluatePermission.bind(permissions));

  // Step 4: Create remaining domain services
  return {
    core,
    permissions,
    sharedData: new SharedDataService(core),
    notifications: new NotificationService(core),
    dashboard: new DashboardService(core),
    dataExport: new DataExportService(core),
  };
}

DataLayerService

The central data orchestrator. Every read and write flows through this service, which applies permission checking, caching, sync broadcasting, and webhook emission.

Read Flow

User Request
    |
    v
Permission Check  -----> DENIED? throw
    |
    v (allowed)
Cache Lookup  ----------> HIT? return cached
    |
    v (miss)
StorageAdapter.getDocument()
    |
    v
Cache Set (store result)
    |
    v
Return Data

Write Flow

User Request
    |
    v
Permission Check  -----> DENIED? throw
    |
    v (allowed)
StorageAdapter.setDocument() / updateDocument() / deleteDocument()
    |
    v
Cache Invalidate (clear stale entry)
    |
    v
Sync Broadcast (notify other clients)
    |
    v
Webhook Emit (notify external systems)
    |
    v
Return Result

Core Operations

import { DataLayerService } from '@cyber-eco/services';

// All operations require a userId for permission checking
await dataLayer.core.get<User>('requester-id', 'users', 'user-123');

await dataLayer.core.create('requester-id', 'users', {
  name: 'Alice',
  email: 'alice@example.com',
  status: 'active',
});

await dataLayer.core.update('requester-id', 'users', 'user-123', {
  name: 'Alice Updated',
});

await dataLayer.core.delete('requester-id', 'users', 'user-123');

// Queries with filters and pagination
const result = await dataLayer.core.query<User>('requester-id', 'users', [
  { field: 'status', operator: '==', value: 'active' },
], {
  sort: [{ field: 'createdAt', direction: 'desc' }],
  limit: 25,
});

// Real-time subscriptions
const unsubscribe = dataLayer.core.subscribe<User>(
  'requester-id', 'users', 'user-123',
  (user) => console.log('User updated:', user)
);

// Batch writes
await dataLayer.core.batchWrite('requester-id', [
  { type: 'set', collection: 'users', id: 'u1', data: { name: 'Alice' } },
  { type: 'update', collection: 'users', id: 'u2', data: { name: 'Bob' } },
  { type: 'delete', collection: 'users', id: 'u3' },
]);

Domain Services

All domain services receive IDataLayerService via constructor injection. They contain no direct database calls.

SharedDataService

Manages data shared across CyberEco apps -- groups, profiles, relationships.

const { sharedData } = dataLayer;

// Cross-app shared profiles, groups, activities
await sharedData.getSharedProfile('user-123');
await sharedData.getSharedGroups('user-123');

PermissionService

Evaluates permissions against the 4-tier role hierarchy and resource-level access controls.

const { permissions } = dataLayer;

// Check if a user can perform an action
const allowed = await permissions.evaluatePermission(
  'user-123',  // userId
  'users',     // collection
  'write',     // action: 'read' | 'write' | 'delete'
  'user-456'   // documentId (optional)
);

// Get all permissions for a user
const userPerms = await permissions.getUserPermissions('user-123');

// Grant a permission
await permissions.grantPermission('admin-id', {
  appId: 'justsplit',
  roles: ['moderator'],
  features: ['manage-groups'],
  grantedAt: new Date().toISOString(),
  grantedBy: 'admin-id',
});

NotificationService

Manages user notifications with delivery status tracking.

const { notifications } = dataLayer;

await notifications.getForUser('user-123');
await notifications.markAsRead('notification-id');
await notifications.create({
  userId: 'user-123',
  type: 'permission_granted',
  title: 'New permission',
  message: 'You have been granted moderator access.',
});

DashboardService

Aggregates data for dashboard views.

const { dashboard } = dataLayer;

const summary = await dashboard.getUserDashboard('user-123');
// Returns aggregated stats: apps, activity, notifications, etc.

DataExportService

Exports user data for GDPR compliance, backups, or migration.

const { dataExport } = dataLayer;

// Export all user data
const exportData = await dataExport.exportUserData('user-123');

// Export specific collections
const partialExport = await dataExport.exportCollections('user-123', [
  'users', 'transactions', 'notifications',
]);

Core Infrastructure Services

These services are used internally by DataLayerService but can be used directly for advanced cases.

CacheService

LRU cache with configurable TTL per collection.

import { CacheService, InMemoryLRUCache } from '@cyber-eco/services';

SyncService

Broadcasts data changes to other connected clients.

import { SyncService } from '@cyber-eco/services';

WebhookService

Emits events to registered webhook endpoints when data changes.

import { WebhookService } from '@cyber-eco/services';

QueryService

Handles query construction and pagination logic.

import { QueryService } from '@cyber-eco/services';

Testing

MockStorageAdapter

The @cyber-eco/services/testing entry point provides MockStorageAdapter, an in-memory implementation of StorageAdapter for unit tests. No Firebase emulators needed.

import { MockStorageAdapter } from '@cyber-eco/services/testing';
import { createDataLayer } from '@cyber-eco/services';

describe('UserService', () => {
  let mock: MockStorageAdapter;
  let dataLayer: ReturnType<typeof createDataLayer>;

  beforeEach(() => {
    mock = new MockStorageAdapter();
    dataLayer = createDataLayer({
      adapter: mock,
      permissions: { enabled: false },  // Disable for unit tests
      sync: { enabled: false },
      webhooks: { enabled: false },
    });
  });

  afterEach(() => {
    mock.clear();
  });

  it('should create and retrieve a user', async () => {
    // Seed test data
    mock.seed('users', 'user-1', {
      name: 'Alice',
      email: 'alice@example.com',
      status: 'active',
    });

    const user = await dataLayer.core.get('system', 'users', 'user-1');
    expect(user).toEqual({
      name: 'Alice',
      email: 'alice@example.com',
      status: 'active',
    });
  });

  it('should query active users', async () => {
    mock.seed('users', 'u1', { name: 'Alice', status: 'active' });
    mock.seed('users', 'u2', { name: 'Bob', status: 'inactive' });
    mock.seed('users', 'u3', { name: 'Carol', status: 'active' });

    const result = await dataLayer.core.query('system', 'users', [
      { field: 'status', operator: '==', value: 'active' },
    ]);

    expect(result.data).toHaveLength(2);
  });
});

MockStorageAdapter API

Method Description
seed(collection, id, data) Pre-populate test data
dump() Return all stored data for assertions
clear() Reset all data, counters, and subscribers
All StorageAdapter methods Full implementation with in-memory maps

Disable Side Effects in Tests

When unit testing, disable permissions, sync, and webhooks in the DataLayerConfig:

createDataLayer({
  adapter: mock,
  permissions: { enabled: false },
  sync: { enabled: false },
  webhooks: { enabled: false },
});

Test Results

Current test status:

Suite Status
Firebase adapter tests 16/16 passing
Services domain tests 27/32 passing (5 OOM -- vitest infra issue)
Auth No test files yet (--passWithNoTests)

Full Bootstrap Example

A complete example wiring all packages together:

// bootstrap.ts
import { initializeFirebase, getHubFirestore, FirebaseStorageAdapter } from '@cyber-eco/firebase';
import { createDataLayer } from '@cyber-eco/services';
import type { CyberEcoDataLayer } from '@cyber-eco/services';
import { COLLECTIONS } from '@cyber-eco/types';

// Initialize Firebase
initializeFirebase({
  hubConfig: {
    apiKey: process.env.FIREBASE_API_KEY!,
    authDomain: process.env.FIREBASE_AUTH_DOMAIN!,
    projectId: process.env.FIREBASE_PROJECT_ID!,
    storageBucket: process.env.FIREBASE_STORAGE_BUCKET!,
    messagingSenderId: process.env.FIREBASE_MESSAGING_SENDER_ID!,
    appId: process.env.FIREBASE_APP_ID!,
  },
  useEmulators: process.env.NODE_ENV === 'development',
  emulatorPorts: { auth: 9099, firestore: 8080 },
});

// Create the data layer
const adapter = new FirebaseStorageAdapter(() => getHubFirestore());

export const dataLayer: CyberEcoDataLayer = createDataLayer({
  adapter,
  cache: { strategy: 'lru', maxSize: 1000, defaultTtl: 300 },
  permissions: { enabled: true },
  sync: { enabled: true },
  webhooks: { enabled: false },
});

// Usage in your application
async function main() {
  const userId = 'current-user-id';

  // Read
  const user = await dataLayer.core.get(userId, COLLECTIONS.USERS, userId);

  // Write
  await dataLayer.core.update(userId, COLLECTIONS.USERS, userId, {
    lastLoginAt: new Date().toISOString(),
  });

  // Query
  const notifications = await dataLayer.core.query(
    userId,
    COLLECTIONS.NOTIFICATIONS,
    [{ field: 'userId', operator: '==', value: userId }],
    { sort: [{ field: 'createdAt', direction: 'desc' }], limit: 10 }
  );

  // Domain services
  const dashboard = await dataLayer.dashboard.getUserDashboard(userId);
  const permissions = await dataLayer.permissions.getUserPermissions(userId);
}

Exports Summary

Main Entry Point (@cyber-eco/services)

Category Exports
Core DataLayerService, CacheService, InMemoryLRUCache, SyncService, WebhookService, QueryService
Domain SharedDataService, PermissionService, NotificationService, DashboardService, DataExportService
Factory createDataLayer, CyberEcoDataLayer

Testing Entry Point (@cyber-eco/services/testing)

Category Exports
Mock MockStorageAdapter