@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¶
| 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:
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.
SyncService¶
Broadcasts data changes to other connected clients.
WebhookService¶
Emits events to registered webhook endpoints when data changes.
QueryService¶
Handles query construction and pagination logic.
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:
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 |